TanStack Query — kesh, qayta yuklash, mutatsiyalar

~25 daq · React Native · Ma'lumot va API

Muammodan boshlaylik

O'tgan darsda bitta ro'yxat uchun 40 qator kod yozdik. Endi tasavvur qiling:

  • Katalogdan mahsulotga o'tib, qaytdingiz — ro'yxat qaytadan yuklanmoqda (spinner)
  • Mahsulot ekranini ochganda — ro'yxatda allaqachon bor ma'lumot yana so'raladi
  • Savatga qo'shdingiz — tab badge va savat ekrani o'zini yangilamaydi
  • Uchta ekran bir xil "profil" ni so'raydi — uchta so'rov
  • Ilova fondan qaytganda ma'lumot eskirgan

Bularning hammasi — server holati muammolari. TanStack Query (avvalgi nomi React Query) ularni avtomatik hal qiladi.


O'rnatish

npm install @tanstack/react-query
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,     // 1 daqiqa — ma'lumot "yangi" hisoblanadi
      retry: 2,              // xatoda 2 marta qayta urinish
    },
  },
});

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <Stack />
    </QueryClientProvider>
  );
}

useQuery — ma'lumot o'qish

O'tgan darsdagi 40 qator:

import { useQuery } from '@tanstack/react-query';
import { apiGet } from '@/lib/api';

export default function Katalog() {
  const { data, isPending, isError, error, refetch, isRefetching } = useQuery({
    queryKey: ['mahsulotlar'],
    queryFn: () => apiGet<Mahsulot[]>('/mahsulotlar'),
  });

  if (isPending) return <ActivityIndicator size="large" style={{ flex: 1 }} />;
  if (isError) return <XatoEkran xabar={error.message} qaytaUrinish={refetch} />;

  return (
    <FlatList
      data={data}
      keyExtractor={(m) => String(m.id)}
      renderItem={({ item }) => <MahsulotKarta mahsulot={item} />}
      refreshing={isRefetching}
      onRefresh={refetch}
    />
  );
}
  • queryKey — ma'lumotning noyob nomi. Bir xil kalit — bir xil kesh
  • queryFn — ma'lumotni olish funksiyasi
  • isPending — birinchi yuklanish; isRefetching — fonda yangilanish
  • Bekor qilish, qayta urinish, takroriy so'rovlarni birlashtirish — avtomatik

Keshning ishlashi

1. Katalog ochildi        → so'rov, natija keshga: ['mahsulotlar']
2. Mahsulotga o'tildi
3. Katalogga qaytildi      → keshdan DARHOL ko'rsatiladi (spinner yo'q)
                           → staleTime o'tgan bo'lsa — fonda jimgina yangilanadi

Foydalanuvchi hech qachon bo'sh ekranni kutmaydi — eski ma'lumotni darhol ko'radi, yangisi fonda keladi. Bu stale-while-revalidate.

Parametrli so'rovlar

function useMahsulot(id: string) {
  return useQuery({
    queryKey: ['mahsulot', id],                       // id — kalitning qismi
    queryFn: () => apiGet<Mahsulot>(`/mahsulotlar/${id}`),
    enabled: !!id,                                    // id bo'lmasa — so'ramaslik
  });
}

function useKatalog(toifa: string, saralash: string) {
  return useQuery({
    queryKey: ['mahsulotlar', { toifa, saralash }],   // filtr o'zgarsa — yangi kesh
    queryFn: () => apiGet<Mahsulot[]>(`/mahsulotlar?toifa=${toifa}&saralash=${saralash}`),
  });
}

Har bir filtr kombinatsiyasi — alohida kesh. "Telefonlar" dan "Noutbuklar" ga o'tib, qaytsangiz — telefonlar darhol ko'rinadi.

Custom hook lar — useMahsulot, useKatalog — qoida: so'rov mantig'i ekranlarda emas, hooks/ yoki api/ da.


useMutation — ma'lumot o'zgartirish

import { useMutation, useQueryClient } from '@tanstack/react-query';

function useSavatgaQosh() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (d: { mahsulotId: number; soni: number }) => apiPost('/savat', d),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['savat'] });   // savatni qayta yuklash
    },
  });
}

// Ekranda
const qosh = useSavatgaQosh();

<Tugma
  matn="Savatga qo'shish"
  yuklanmoqda={qosh.isPending}
  onPress={() => qosh.mutate({ mahsulotId: mahsulot.id, soni: 1 }, {
    onSuccess: () => Alert.alert("Qo'shildi"),
    onError: (e) => Alert.alert('Xato', e.message),
  })}
/>

invalidateQueries(['savat']) — "savat haqidagi kesh eskirdi". Savat ekrani, tab badge — ['savat'] dan foydalanadigan hamma joy avtomatik yangilanadi. Qo'lda hech narsa uzatish shart emas.

Optimistik yangilash

Serverdan javob kutmasdan, darhol interfeysni yangilash — "yoqtirish" tugmasi kabi:

useMutation({
  mutationFn: (id: number) => apiPost(`/mahsulotlar/${id}/sevimli`, {}),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['mahsulot', String(id)] });
    const oldingi = queryClient.getQueryData<Mahsulot>(['mahsulot', String(id)]);
    queryClient.setQueryData<Mahsulot>(['mahsulot', String(id)], (m) => m && { ...m, sevimli: !m.sevimli });
    return { oldingi };
  },
  onError: (_e, id, ctx) => {
    queryClient.setQueryData(['mahsulot', String(id)], ctx?.oldingi);   // xato — qaytarish
  },
  onSettled: (_d, _e, id) => {
    queryClient.invalidateQueries({ queryKey: ['mahsulot', String(id)] });
  },
});

Yurak darhol qizaradi; server xato qaytarsa — qaytib oqaradi.


Cheksiz scroll: useInfiniteQuery

20-darsdagi qo'lda yozilgan mantiq:

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isRefetching } = useInfiniteQuery({
  queryKey: ['yangiliklar'],
  queryFn: ({ pageParam }) => apiGet<{ items: Yangilik[]; keyingi: number | null }>(`/yangiliklar?kursor=${pageParam}`),
  initialPageParam: 0,
  getNextPageParam: (oxirgi) => oxirgi.keyingi,      // null — boshqa sahifa yo'q
});

const yangiliklar = data?.pages.flatMap((p) => p.items) ?? [];

<FlatList
  data={yangiliklar}
  onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
  onEndReachedThreshold={0.5}
  ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
  refreshing={isRefetching}
  onRefresh={refetch}
/>

React Native ga xos sozlamalar

Vebda TanStack Query oyna fokusga kelganda ma'lumotni yangilaydi. Mobil ilovada "oyna" yo'q — lekin ilova fondan qaytadi:

// app/_layout.tsx
import { AppState, Platform } from 'react-native';
import { focusManager, onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';

// Ilova fondan qaytganda — eskirgan so'rovlarni yangilash
useEffect(() => {
  const sub = AppState.addEventListener('change', (holat) => {
    if (Platform.OS !== 'web') focusManager.setFocused(holat === 'active');
  });
  return () => sub.remove();
}, []);

// Internet qaytganda — avtomatik qayta urinish
onlineManager.setEventListener((setOnline) =>
  NetInfo.addEventListener((s) => setOnline(!!s.isConnected))
);

Ekranga qaytganda yangilash (26-darsdagi useFocusEffect muammosi) — TanStack Query staleTime bilan buni ko'pincha o'zi hal qiladi.


Nima uchun shunday?

Nima uchun server ma'lumotini oddiy useState yoki Zustand da saqlamaslik kerak?

Chunki server ma'lumoti — sizniki emas. U boshqa joyda yashaydi, boshqalar uni o'zgartiradi, u eskiradi. Uni boshqarish — keshlash, qachon yangilash, takroriy so'rovlarni birlashtirish, xatoda qayta urinish, sahifalash — murakkab va har bir ilovada bir xil muammo. Klient holati (tanlangan tab, ochiq modal, savat qoralamasi) esa butunlay sizniki va oddiy. TanStack Query ikkalasini ajratadi: server holati — Query da, klient holati — useState / Zustand da (keyingi dars).


Xulosa

Eslab qoling:

  • QueryClientProvider — ildizda; staleTime, retry — umumiy sozlamalar
  • useQuery({ queryKey, queryFn }) — isPending, isError, data, refetch
  • queryKey ga barcha parametrlar kiradi; enabled — shartli so'rov
  • Kesh: darhol eski ma'lumot, fonda yangilanish
  • useMutation + invalidateQueries — o'zgarishdan keyin bog'liq joylar o'zi yangilanadi
  • Optimistik yangilash — onMutate / onError bilan qaytarish
  • useInfiniteQuery — cheksiz scroll
  • focusManager + AppState, onlineManager + NetInfo — mobilga moslash

Keyingi dars

33-dars: Zustand — global holat (savat, sozlamalar)