Maîtriser TanStack Query

useQuery, useMutation, caching, invalidation & optimistic updates

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Server state vs client state

Comprendre la différence fondamentale

2. useQuery

Fetching déclaratif avec loading/error/data

3. Query keys & cache

Système de cache intelligent

4. useMutation + invalidation

Pattern CRUD complet

5. staleTime vs gcTime

Configurer le caching

6. Optimistic updates

UI instantanée et rollback

Plan du cours

1

Server state vs client state

Pourquoi c'est un problème différent

2

useQuery : fetching déclaratif

Loading/error/data automatiques

3

Query keys : le système de cache

Hiérarchie et key factories

4

useMutation + invalidateQueries

Le pattern CRUD complet

5

Live coding

Transformer un fetch manuel en TanStack Query

Server State vs Client State

Pourquoi c'est un problème différent

Le state serveur a des contraintes uniques

Qui ne s'appliquent pas au state client

Caractéristiques du Server State

🌐

Stocké distamment

Les données vivent sur un serveur, pas en mémoire locale

Asynchrone

Nécessite des API calls avec loading/error states

🔄

Peut devenir obsolète

Les données changent sur le serveur sans notification

👥

Propriété partagée

D'autres utilisateurs peuvent modifier les données

Client State vs Server State

Critère Client State Server State
Stockage Mémoire locale Serveur distant
Accès Synchrone Asynchrone
Propriétaire L'application Le serveur
Obsolescence Non Oui (stale)
Outil recommandé Zustand / Context TanStack Query

Ne mélangez pas : Zustand = client state, TanStack Query = server state

Le problème du fetching manuel

Beaucoup de boilerplate répétitif

useState pour data, loading, error

useEffect pour déclencher le fetch

❌ Gestion manuelle du cleanup et des race conditions

❌ Pas de cache, pas de retry, pas de deduplication

useQuery

Fetching déclaratif sans boilerplate

Une seule fonction : 3 états gérés

data, isLoading, isError

Syntaxe de base

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

const { data, isLoading, isError } = useQuery({

queryKey: ['users'],

queryFn: async () => {

const res = await fetch('/api/users')

if (!res.ok) throw new Error('Erreur')

return res.json()

}

})

queryKey

Identifiant unique pour le cache

queryFn

Fonction async qui fetch les données

États automatiques — Plus de useState!

isLoading

Premier chargement

Pas encore de données

isSuccess

data disponible

data !== undefined

isError

Échec du fetch

error disponible

Bonus : isFetching = true pendant tout fetch (y compris refetch)

Exemple complet : UserList

function UserList() {

const { data: users, isLoading, isError } = useQuery({

queryKey: ['users'],

queryFn: () => axios.get('/api/users').then(r => r.data)

})

if (isLoading) return <p>Chargement...</p>

if (isError) return <p>Erreur!</p>

return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>

}

✅ 10 lignes vs 30+ avec useEffect + useState

Comportements par défaut

TanStack Query est intelligent par défaut

🔄

refetchOnWindowFocus

Refetch automatiquement quand l'utilisateur revient sur l'onglet

🔁

retry (3x)

Retry automatique sur erreur (3 tentatives)

🎯

Deduplication

Une seule requête même si plusieurs composants utilisent la même query key

Conditional fetching avec enabled

const { data } = useQuery({

queryKey: ['user', userId],

queryFn: () => fetchUser(userId),

enabled: !!userId // false si userId est null/undefined

})

💡 La query ne s'exécute que si enabled: true

Utile pour les dépendances entre requêtes

Query Keys

Le système de cache intelligent

Chaque query est identifiée par une clé unique

Utilisée pour le cache, l'invalidation et le partage

Structure hiérarchique

Les query keys sont des tableaux — plus spécifiques vers la droite

['todos'] // Tous les todos

['todos', 5] // Todo avec id=5

['todos', { status: 'done' }] // Todos filtrés

['todos', 5, { preview: true }] // Todo 5 en preview

💡 Plus la clé est spécifique, plus le cache est granulaire

Pattern : Key Factory

Centraliser la gestion des clés pour éviter les erreurs

export const todoKeys = {

all: ['todos'] as const,

lists: () => [...todoKeys.all, 'list'] as const,

list: (filter: string) => [...todoKeys.lists(), filter] as const,

details: () => [...todoKeys.all, 'detail'] as const,

detail: (id: number) => [...todoKeys.details(), id] as const,

}

todoKeys.all pour invalider tous les todos

todoKeys.detail(5) pour un todo spécifique

Utilisation avec useQuery

const { data } = useQuery({

queryKey: todoKeys.list('done'),

queryFn: () => fetchTodos({ status: 'done' })

})

Avantages :

  • • Autocomplétion dans l'IDE
  • • Pas de typo possible
  • • Refactoring facile
  • • Invalidation partielle ou totale

Le Cache Intelligent

staleTime vs gcTime — la distinction critique

TanStack Query cache automatiquement les données

Mais quand refetch ? Quand supprimer ?

staleTime vs gcTime

staleTime

Combien de temps les données sont-elles fraîches ?

Défaut : 0 (immédiatement stale)

Pendant staleTime : pas de refetch

Après staleTime : refetch en arrière-plan

gcTime (garbage collection)

Combien de temps garder les données inutilisées ?

Défaut : 5 min (300000ms)

Après gcTime : supprimé du cache

Compte à rebours si aucun composant n'utilise la query

staleTime — Quand refetch ?

const { data } = useQuery({

queryKey: ['users'],

queryFn: fetchUsers,

staleTime: 5 * 60 * 1000 // 5 minutes

})

Pendant 5 minutes : pas de refetch, même si :

  • • L'utilisateur change d'onglet et revient
  • • Un nouveau composant mount avec la même query
  • • Le réseau redevient disponible

gcTime — Quand supprimer du cache ?

const { data } = useQuery({

queryKey: ['user', id],

queryFn: () => fetchUser(id),

gcTime: 10 * 60 * 1000 // 10 minutes

})

Scénario :

  1. 1. L'utilisateur quitte la page (query inutilisée)
  2. 2. Le timer gcTime démarre (10 min)
  3. 3. Si l'utilisateur revient avant 10 min → données encore là
  4. 4. Après 10 min → données supprimées, refetch nécessaire

Timeline du Cache

Fetch

Stale

Garbage Collected

← staleTime → ← gcTime →

Frais

Utilise le cache

Stale

Refetch en background

Supprimé

Nouveau fetch

useMutation

Pour les opérations d'écriture (POST, PUT, DELETE)

Créer, modifier, supprimer — pas de cache automatique

Mais on peut invalider les queries existantes

Syntaxe de base

const mutation = useMutation({

mutationFn: async (newTodo) => {

const { data } = await axios.post('/api/todos', newTodo)

return data

}

})

// Déclencher la mutation :

mutation.mutate({ title: 'Nouvelle tâche' })

isPending

En cours

isSuccess

Réussi

isError

Échoué

Callback onSuccess

const mutation = useMutation({

mutationFn: createTodo,

onSuccess: (data, variables) => {

console.log('Créé:', data)

// Rediriger, fermer modal, etc.

},

onError: (error) => {

toast.error(error.message)

}

})

variables = les arguments passés à mutate()

invalidateQueries

Le pattern standard après une mutation

Invalider = marquer comme stale

Déclenche un refetch automatique si la query est active

Pattern CRUD complet

const queryClient = useQueryClient()

const mutation = useMutation({

mutationFn: createTodo,

onSuccess: () => {

// Invalider toutes les queries 'todos'

queryClient.invalidateQueries({

queryKey: todoKeys.all()

})

}

})

💡 Après create/update/delete → invalider la liste pour la rafraîchir

Exemple complet : Create Post

function CreatePost() {

const queryClient = useQueryClient()

const mutation = useMutation({

mutationFn: (post) => api.post('/posts', post),

onSuccess: () => {

queryClient.invalidateQueries({ queryKey: ['posts'] })

navigate('/posts')

}

})

return (

<form onSubmit={(e) => {

e.preventDefault()

mutation.mutate({ title, body })

}}>

{mutation.isPending ? 'Création...' : 'Créer'}

</form>

)

}

Optimistic Updates

UI instantanée avant la réponse du serveur

Mettre à jour le cache immédiatement

Et annuler si le serveur répond avec une erreur

Le pattern complet

1

onMutate

Annuler les queries en cours + sauvegarder l'ancien state + mettre à jour optimistement

2

onError

Restaurer l'ancien state (rollback)

3

onSettled

Invalider pour refetch les vraies données

Exemple : Toggle Todo optimiste

const mutation = useMutation({

mutationFn: toggleTodo,

onMutate: async (id) => {

await queryClient.cancelQueries({ queryKey: ['todos'] })

const previous = queryClient.getQueryData(['todos'])

queryClient.setQueryData(['todos'], (old) =>

old.map(t => t.id === id ? {...t, done: !t.done} : t)

)

return { previous }

},

Rollback en cas d'erreur

onError: (err, id, context) => {

queryClient.setQueryData(['todos'], context.previous)

},

onSettled: () => {

queryClient.invalidateQueries({ queryKey: ['todos'] })

}

✅ L'utilisateur voit le changement immédiatement

Si erreur, on revient à l'état précédant transparent

React Query DevTools

Explorer le cache en temps réel

Extension navigateur + composant intégré

Indispensable pour le debugging

Fonctionnalités

🔍 Query Explorer

Voir toutes les queries actives, leur state et leurs données

📊 Cache Inspector

Examiner le contenu du cache en détail

⚡ Actions rapides

Refetch, invalidate, reset directement depuis les DevTools

🎬 Timeline

Voir l'historique des mutations et queries

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<ReactQueryDevtools initialIsOpen={false} />

Setup obligatoire

QueryClientProvider dans le tree React

Sans ça, rien ne fonctionne

Erreur : "No QueryClient set"

Configuration dans main.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({

defaultOptions: {

queries: {

staleTime: 1000 * 60, // 1 minute par défaut

}

}

})

root.render(

<QueryClientProvider client={queryClient}>

<App />

</QueryClientProvider>

)

⚠️ Pièges courants

Les erreurs à éviter absolument

Piège #1 : Oublier QueryClientProvider

Erreur classique :

Error: No QueryClient set, use QueryClientProvider to set one

✅ Solution : Envelopper l'app avec QueryClientProvider

Piège #2 : Query keys plates

❌ Mauvais

['users']

['user-5']

['user-5-detail']

Impossible d'invalider tous les users

✅ Bon

['users']

['users', 5]

['users', 5, 'detail']

Invalider ['users'] → tous les caches users

Piège #3 : Ne pas invalider après mutation

Symptôme :

"J'ai créé un post mais la liste ne se met pas à jour !"

✅ Solution : Toujours appeler invalidateQueries dans onSuccess

queryClient.invalidateQueries({ queryKey: ['posts'] })

Piège #4 : Confondre staleTime et gcTime

staleTime

Quand les données deviennent obsolètes

→ Déclenche un refetch en background

gcTime

Quand les données sont supprimées du cache

→ Nouveau fetch nécessaire

💡 Mnémo : stale = rassis (encore là mais à rafraîchir), gc = garbage collected (supprimé)

Récapitulatif

useQuery

Fetching déclaratif

queryKey

Identifiant de cache hiérarchique

staleTime

Durée de fraîcheur

gcTime

Durée de rétention

useMutation

Opérations d'écriture

invalidateQueries

Rafraîchir le cache

Ressources

📚

Documentation officielle

https://tanstack.com/query/latest

🎥

TkDodo's Blog

https://tkdodo.eu/blog/practical-react-query

🔧

React Query DevTools

Extension Chrome/Firefox

Questions ? 🤔