Kurs React Native · Moduł 4: Stan i dane z API
React Query (TanStack Query) - Zaawansowane pobieranie danych
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-queryKonfiguracja
// 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. Co jest wymagane w useQuery?
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.