Kurs React Native · Moduł 4: Stan i dane z API

React Query (TanStack Query) - Zaawansowane pobieranie danych

3 min czytania ·
W tej lekcji7

TanStack Query to potężna biblioteka do zarządzania stanem serwera - cachowanie, odświeżanie, synchronizacja i więcej!

Instalacja

npx expo install @tanstack/react-query

Konfiguracja

// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minut
      gcTime: 10 * 60 * 1000, // 10 minut (garbage collection)
      retry: 3,
      refetchOnWindowFocus: false, // wyłącz na mobile
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <NavigationContainer>
        {/* ... */}
      </NavigationContainer>
    </QueryClientProvider>
  );
}

useQuery - pobieranie danych

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

function CreatureList() {
  const {
    data: creatures,
    isLoading,
    isError,
    error,
    refetch,
    isRefetching,
  } = useQuery({
    queryKey: ['creatures'], // unikalny klucz dla cache
    queryFn: creatureService.getAll,
  });

  if (isLoading) {
    return <ActivityIndicator size="large" />;
  }

  if (isError) {
    return (
      <View>
        <Text>Błąd: {error.message}</Text>
        <Pressable onPress={() => refetch()}>
          <Text>Spróbuj ponownie</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <FlatList
      data={creatures}
      refreshing={isRefetching}
      onRefresh={refetch}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Text>{item.name}</Text>
      )}
    />
  );
}

Parametryczne query

function CreatureDetail({ creatureId }: { creatureId: string }) {
  const { data: creature, isLoading } = useQuery({
    queryKey: ['creature', creatureId], // klucz z parametrem
    queryFn: () => creatureService.getById(creatureId),
    enabled: !!creatureId, // wykonaj tylko gdy jest ID
  });

  // ...
}

// Wyszukiwanie z debounce
function SearchCreatures() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);

  const { data: results } = useQuery({
    queryKey: ['creatures', 'search', debouncedSearch],
    queryFn: () => creatureService.search(debouncedSearch),
    enabled: debouncedSearch.length > 2,
  });

  return (
    <View>
      <TextInput
        value={search}
        onChangeText={setSearch}
        placeholder="Szukaj stworzeń..."
      />
      <FlatList data={results} /* ... */ />
    </View>
  );
}

useMutation - modyfikacja danych

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

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

  const createMutation = useMutation({
    mutationFn: creatureService.create,
    onSuccess: (newCreature) => {
      // Opcja 1: Invalidate - ponowne pobranie
      queryClient.invalidateQueries({ queryKey: ['creatures'] });

      // Opcja 2: Optymistyczna aktualizacja cache
      queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) => {
        return old ? [...old, newCreature] : [newCreature];
      });
    },
    onError: (error) => {
      Alert.alert('Błąd', 'Nie udało się utworzyć stworzenia');
    },
  });

  const handleSubmit = (data: Omit<Creature, 'id'>) => {
    createMutation.mutate(data);
  };

  return (
    <View>
      {/* Formularz */}
      <Pressable
        onPress={() => handleSubmit({ name: 'Trytan', species: 'Tryton', habitat: 'Głębiny' })}
        disabled={createMutation.isPending}
      >
        <Text>{createMutation.isPending ? 'Tworzenie...' : 'Utwórz'}</Text>
      </Pressable>
    </View>
  );
}

Optymistyczne aktualizacje

const deleteMutation = useMutation({
  mutationFn: creatureService.delete,
  onMutate: async (creatureId) => {
    // Anuluj oczekujące query
    await queryClient.cancelQueries({ queryKey: ['creatures'] });

    // Snapshot poprzedniego stanu
    const previousCreatures = queryClient.getQueryData(['creatures']);

    // Optymistycznie usuń
    queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) =>
      old?.filter((c) => c.id !== creatureId)
    );

    // Zwróć kontekst z poprzednim stanem
    return { previousCreatures };
  },
  onError: (err, creatureId, context) => {
    // Przywróć poprzedni stan przy błędzie
    queryClient.setQueryData(['creatures'], context?.previousCreatures);
  },
  onSettled: () => {
    // Zawsze odśwież po zakończeniu
    queryClient.invalidateQueries({ queryKey: ['creatures'] });
  },
});

Infinite Query - paginacja

import { useInfiniteQuery } from '@tanstack/react-query';

function InfiniteCreatureList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['creatures', 'infinite'],
    queryFn: ({ pageParam = 1 }) =>
      creatureService.getPaginated(pageParam, 20),
    getNextPageParam: (lastPage, pages) =>
      lastPage.hasMore ? pages.length + 1 : undefined,
    initialPageParam: 1,
  });

  const creatures = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <FlatList
      data={creatures}
      onEndReached={() => hasNextPage && fetchNextPage()}
      onEndReachedThreshold={0.5}
      ListFooterComponent={() =>
        isFetchingNextPage ? <ActivityIndicator /> : null
      }
      // ...
    />
  );
}

TanStack Query znacząco upraszcza zarządzanie stanem serwera i automatyzuje cachowanie, odświeżanie i synchronizację!

Kod do tej lekcji: App.tsx
import React from 'react';
import { View, Text, Pressable, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

const queryClient = new QueryClient();

interface Creature {
  id: string;
  name: string;
}

// Symulacja zapytania do API
async function fetchCreatures(): Promise<Creature[]> {
  await new Promise((resolve) => setTimeout(resolve, 800));
  return [
    { id: '1', name: 'Tryton' },
    { id: '2', name: 'Syrena' },
    { id: '3', name: 'Kraken' },
  ];
}

function CreatureList() {
  // TODO: użyj useQuery z queryKey: ['creatures'] i queryFn: fetchCreatures
  // Wyciągnij: data, isLoading, isError, refetch, isRefetching
  const {
    data: creatures,
    isLoading,
    isError,
    refetch,
    isRefetching,
  } = useQuery({
    queryKey: ['creatures'],
    queryFn: fetchCreatures,
  });

  // TODO: gdy isLoading === true, pokaż <ActivityIndicator />
  if (isLoading) {
    return <ActivityIndicator size="large" color="#fff" style={styles.center} />;
  }

  // TODO: gdy isError === true, pokaż komunikat błędu i przycisk Spróbuj ponownie (onPress={refetch})
  if (isError) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>Błąd pobierania danych</Text>
        <Pressable style={styles.button} onPress={() => refetch()}>
          <Text style={styles.buttonText}>Spróbuj ponownie</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <FlatList
      style={styles.list}
      data={creatures}
      refreshing={isRefetching}
      onRefresh={refetch}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text style={styles.item}>{item.name}</Text>}
    />
  );
}

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

const styles = StyleSheet.create({
  list: { flex: 1, padding: 20, backgroundColor: '#1a5276' },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#1a5276',
  },
  item: { color: '#aed6f1', fontSize: 18, paddingVertical: 8 },
  error: { color: '#f1948a', fontSize: 16, marginBottom: 16 },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    backgroundColor: '#2980b9',
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co jest wymagane w useQuery?

  2. 2. Co oznacza staleTime w React Query?

To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Klikanie w kolejności

    Kliknij elementy useQuery w poprawnej kolejności:

  • Układanie w poziomie

    Ułóż invalidację cache po mutacji:

  • Edytor kodu

    Uzupełnij kod z obsługą loading, error i refetch.

Przydatne artykuły