Cours React Native · Module 4: État et données d'API

React Query (TanStack Query) - Récupération avancée de données

4 min de lecture ·
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-query

Configuration

// 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. 1. Qu'est-ce qui est obligatoire dans useQuery ?

  2. 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.

Articles utiles