Cours React Native · Module 4: État et données d'API
React Query (TanStack Query) - Récupération avancée de données
Dans cette leçon7
TanStack Query est une puissante bibliothèque pour gérer l'état serveur - mise en cache, rafraîchissement, synchronisation, et bien plus encore !
Installation
npx expo install @tanstack/react-queryConfiguration
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (garbage collection)
retry: 3,
refetchOnWindowFocus: false, // désactiver sur mobile
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<NavigationContainer>
{/* ... */}
</NavigationContainer>
</QueryClientProvider>
);
}useQuery - Récupérer des données
import { useQuery } from '@tanstack/react-query';
import { creatureService } from '@/api/creatureService';
function CreatureList() {
const {
data: creatures,
isLoading,
isError,
error,
refetch,
isRefetching,
} = useQuery({
queryKey: ['creatures'], // clé de cache unique
queryFn: creatureService.getAll,
});
if (isLoading) {
return <ActivityIndicator size="large" />;
}
if (isError) {
return (
<View>
<Text>Erreur : {error.message}</Text>
<Pressable onPress={() => refetch()}>
<Text>Réessayer</Text>
</Pressable>
</View>
);
}
return (
<FlatList
data={creatures}
refreshing={isRefetching}
onRefresh={refetch}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text>{item.name}</Text>
)}
/>
);
}Requête paramétrée
function CreatureDetail({ creatureId }: { creatureId: string }) {
const { data: creature, isLoading } = useQuery({
queryKey: ['creature', creatureId], // clé avec paramètre
queryFn: () => creatureService.getById(creatureId),
enabled: !!creatureId, // ne s'exécute que s'il y a un ID
});
// ...
}
// Recherche avec 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="Rechercher des créatures..."
/>
<FlatList data={results} /* ... */ />
</View>
);
}useMutation - Modifier des données
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateCreatureForm() {
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: creatureService.create,
onSuccess: (newCreature) => {
// Option 1 : Invalider - re-fetch
queryClient.invalidateQueries({ queryKey: ['creatures'] });
// Option 2 : Mise à jour optimiste du cache
queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) => {
return old ? [...old, newCreature] : [newCreature];
});
},
onError: (error) => {
Alert.alert('Erreur', 'Échec de la création de la créature');
},
});
const handleSubmit = (data: Omit<Creature, 'id'>) => {
createMutation.mutate(data);
};
return (
<View>
{/* Formulaire */}
<Pressable
onPress={() => handleSubmit({ name: 'Triton', species: 'Triton', habitat: 'Les Profondeurs' })}
disabled={createMutation.isPending}
>
<Text>{createMutation.isPending ? 'Création...' : 'Créer'}</Text>
</Pressable>
</View>
);
}Mises à jour optimistes
const deleteMutation = useMutation({
mutationFn: creatureService.delete,
onMutate: async (creatureId) => {
// Annuler les requêtes en cours
await queryClient.cancelQueries({ queryKey: ['creatures'] });
// Capturer l'état précédent (snapshot)
const previousCreatures = queryClient.getQueryData(['creatures']);
// Retirer de façon optimiste
queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) =>
old?.filter((c) => c.id !== creatureId)
);
// Retourner le contexte avec l'état précédent
return { previousCreatures };
},
onError: (err, creatureId, context) => {
// Restaurer l'état précédent en cas d'erreur
queryClient.setQueryData(['creatures'], context?.previousCreatures);
},
onSettled: () => {
// Toujours rafraîchir une fois terminé
queryClient.invalidateQueries({ queryKey: ['creatures'] });
},
});Infinite Query - Pagination
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 simplifie considérablement la gestion de l'état serveur et automatise la mise en cache, le rafraîchissement et la synchronisation !
Code de cette leçon : 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;
}
// Requête API simulée
async function fetchCreatures(): Promise<Creature[]> {
await new Promise((resolve) => setTimeout(resolve, 800));
return [
{ id: '1', name: 'Triton' },
{ id: '2', name: 'Sirène' },
{ id: '3', name: 'Kraken' },
];
}
function CreatureList() {
// TODO : utilise useQuery avec queryKey: ['creatures'] et queryFn: fetchCreatures
// Extrais : data, isLoading, isError, refetch, isRefetching
const {
data: creatures,
isLoading,
isError,
refetch,
isRefetching,
} = useQuery({
queryKey: ['creatures'],
queryFn: fetchCreatures,
});
// TODO : quand isLoading === true, affiche <ActivityIndicator />
if (isLoading) {
return <ActivityIndicator size="large" color="#fff" style={styles.center} />;
}
// TODO : quand isError === true, affiche un message d'erreur et un bouton Réessayer (onPress={refetch})
if (isError) {
return (
<View style={styles.center}>
<Text style={styles.error}>Erreur lors de la récupération des données</Text>
<Pressable style={styles.button} onPress={() => refetch()}>
<Text style={styles.buttonText}>Réessayer</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' },
});Entrées de la documentation utilisées dans cette leçon
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce qui est obligatoire dans useQuery ?
2. Que signifie staleTime dans React Query ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Clique sur les éléments de useQuery dans le bon ordre :
- Tri horizontal
Reconstitue l'invalidation du cache après une mutation :
- Éditeur de code
Complète le code avec la gestion du chargement, des erreurs et du refetch.