Skip to Content

Chapitre 7.2 — TanStack Query

Où on en est : fetchJson est robuste mais brut — chaque écran devrait gérer loading/ erreur/cache/refetch à la main. TanStack Query industrialise tout ça.

⏱️ TL;DRTanStack Query gère le cycle de vie des données distantes : cache par queryKey, staleTime (fraîcheur), retries automatiques, refetch intelligent, pull-to-refresh en une ligne. Setup RN : QueryClientProvider dans le layout racine + onlineManager branché sur NetInfo + focusManager sur AppState. Les écrans consomment useQuery({ queryKey, queryFn }) et déclarent leurs trois états.

🎯 Objectifs

  • Installer et configurer Query pour RN (online/focus managers).
  • Comprendre queryKey, staleTime, gcTime — le modèle mental du cache.
  • Écrire les hooks de données de Drill (useThemes, useQuestions).
  • Brancher pull-to-refresh et retry sur l’existant.

Installation & branchement RN

pnpm add @tanstack/react-query
// app/_layout.tsx — le provider + les branchements mobiles import { QueryClient, QueryClientProvider, onlineManager, focusManager } from '@tanstack/react-query' import NetInfo from '@react-native-community/netinfo' import { AppState } from 'react-native' // 1. Query sait si on est en ligne (retries intelligents, refetchOnReconnect) onlineManager.setEventListener(setOnline => NetInfo.addEventListener(state => setOnline(!!state.isConnected)) ) // 2. « focus » = retour de l'app au premier plan (l'équivalent RN du focus d'onglet web) focusManager.setEventListener(handleFocus => { const sub = AppState.addEventListener('change', s => handleFocus(s === 'active')) return () => sub.remove() }) const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 60, // 1 h : les questions ne changent pas si souvent retry: 2, }, }, }) export default function RootLayout() { return ( <QueryClientProvider client={queryClient}> <Stack …/> </QueryClientProvider> ) }

Les deux branchements sont LE setup spécifique RN : sur le web, Query écoute window (online, visibilitychange) ; en RN, on lui donne NetInfo et AppState. Dix lignes, une fois.

Le modèle mental du cache

const { data, isPending, isError, refetch, isRefetching } = useQuery({ queryKey: ['themes'], // l'IDENTITÉ de la donnée queryFn: fetchThemesManifest, // comment l'obtenir })
  • queryKey : l’identité. Deux composants avec la même clé partagent LA même donnée et le même fetch (déduplication automatique). Clé paramétrée : ['questions', slug].
  • staleTime : durée de « fraîcheur ». Donnée fraîche → servie du cache, AUCUN réseau. Donnée périmée (stale) → servie du cache PUIS refetch en arrière-plan (stale-while- revalidate). Pour Drill : 1 h — on rejouera 10 sessions sans re-télécharger.
  • gcTime (défaut 5 min) : combien de temps une donnée inutilisée reste en mémoire.
  • Refetch automatiques : au retour au premier plan (focusManager), à la reconnexion (onlineManager), au remontage si stale. Gratuit.

Les hooks de données de Drill

Convention du cours : un fichier hooks/queries.ts — les écrans ne connaissent jamais les URLs :

// hooks/queries.ts import { useQuery } from '@tanstack/react-query' import { fetchJson } from '../lib/http' import type { Theme, Question } from '../lib/types' const BASE = 'https://raw.githubusercontent.com/<user>/drill-questions/main' export function useThemes() { return useQuery({ queryKey: ['themes'], queryFn: () => fetchJson<Theme[]>(`${BASE}/manifest.json`), }) } export function useQuestions(slug: string) { return useQuery({ queryKey: ['questions', slug], queryFn: () => fetchJson<Question[]>(`${BASE}/themes/${slug}.json`), enabled: !!slug, // pas de fetch tant que le slug n'existe pas }) }

(La validation zod remplace les <Theme[]> optimistes au chapitre suivant — chaque chose en son temps.)

L’écran Thèmes, réécrit sur les vraies données :

// app/(tabs)/themes/index.tsx export default function ThemesScreen() { const { data: themes, isPending, isError, refetch, isRefetching } = useThemes() if (isPending) return <Screen><ActivityIndicator className="mt-16" /></Screen> if (isError) return <ErrorState onRetry={refetch} /> // composant du ch. 7.4 return ( <FlatList data={themes} keyExtractor={t => t.slug} renderItem={…} refreshing={isRefetching} onRefresh={refetch} // pull-to-refresh : DONE contentContainerClassName="gap-3 p-4" /> ) }

Et le lancement d’une partie devient : le détail du thème lit useQuestions(slug), tire N questions au hasard (lib/draw.ts, pur), et start(theme, drawn) — le store de la Partie 6 ne change pas d’un iota. Les couches s’emboîtent.

⚠️ Piège — Résistez à la tentation de copier data dans un useState ou dans Zustand (« pour l’avoir sous la main »). Le cache Query EST votre état serveur — le dupliquer crée deux sources de vérité qui divergent. La frontière du cours : Query = données distantes, Zustand = état de jeu, SQLite = historique local. La session copie les questions tirées (snapshot de partie), pas la banque.

💡 Pour un dev Next.js — C’est le même TanStack Query que côté client web. Ce qui disparaît : les Server Components/fetch serveur cache — ici TOUT passe par Query. Ce qui apparaît : online/focusManager mobiles. staleTime élevé + refetch discrets = le comportement ISR-like, mais dans la poche.

✏️ Exercices

1. Créez lib/draw.ts : drawQuestions(all: Question[], count: number): Question[] — tirage aléatoire sans doublon, mélange des choix PAS nécessaire (l’ordre des choices est fixe), mais l’ordre des questions doit être aléatoire. Pure et testable.

✅ Solution

export function drawQuestions(all: Question[], count: number): Question[] { const pool = [...all] for (let i = pool.length - 1; i > 0; i--) { // Fisher-Yates const j = Math.floor(Math.random() * (i + 1)) ;[pool[i], pool[j]] = [pool[j], pool[i]] } return pool.slice(0, Math.min(count, pool.length)) }

Fisher-Yates (mélange uniforme — sort(() => Math.random()-0.5) est biaisé), borné à la taille de la banque. Pour tester le hasard en P14 : injecter le générateur (rng: () => number en paramètre optionnel).

2. Le détail d’un thème doit précharger les questions dès son ouverture (pour que « Lancer » soit instantané). Trouvez l’API Query adaptée.

✅ Solution

Rien à faire si le détail appelle déjà useQuestions(slug) (le fetch part au montage — le bouton Lancer lira le cache). Alternative explicite depuis la LISTE (précharger au tap « avant » la navigation) : queryClient.prefetchQuery({ queryKey: ['questions', slug], queryFn: … }) dans le onPress. Même clé → le détail trouve la donnée déjà là.

3. Pendant une partie, l’utilisateur pull-to-refresh la liste des thèmes : les questions de la session en cours peuvent-elles changer sous ses pieds ? Argumentez avec l’architecture.

✅ Solution

Non : start() a copié les questions tirées dans le store de session (snapshot). Le refetch met à jour le CACHE (['questions', slug]), pas la session. Prochaine partie = données fraîches ; partie en cours = figée. C’est exactement pourquoi la session duplique le tirage — la seule duplication légitime, car c’est un instantané, pas un miroir.

🧠 Quiz

1. Que garantit une même queryKey utilisée par deux écrans ?

Réponse

Une seule donnée partagée et des fetchs dédupliqués : le second écran lit le cache (et déclenche au plus un refetch si stale) — jamais deux requêtes parallèles identiques.

2. staleTime vs gcTime ?

Réponse

staleTime = durée de fraîcheur (frais → zéro réseau ; stale → servi + refetch en fond). gcTime = durée de rétention en mémoire d’une donnée inutilisée avant éviction.

3. Quels sont les deux branchements RN spécifiques de Query ?

Réponse

onlineManager ← NetInfo (reconnexion) et focusManager ← AppState (retour au premier plan) — les équivalents mobiles des événements window du web.

4. Comment obtient-on le pull-to-refresh avec Query ?

Réponse

refreshing={isRefetching} + onRefresh={refetch} sur la FlatList — Query gère l’état et la requête.

5. Pourquoi ne copie-t-on jamais data dans Zustand — et quelle est l’exception de Drill ?

Réponse

Deux sources de vérité divergent (cache mis à jour, copie périmée). Exception : la session copie les questions tirées — un snapshot de partie voulu figé, pas un miroir de la banque.


👉 Chapitre suivant : 7.3 — Le pattern Drill — le repo drill-questions : schéma, manifest, zod. La pièce maîtresse.