useQuery, useMutation, caching, invalidation & optimistic updates
Utilisez les flèches, cliquez ou glissez pour naviguer
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
Server state vs client state
Pourquoi c'est un problème différent
useQuery : fetching déclaratif
Loading/error/data automatiques
Query keys : le système de cache
Hiérarchie et key factories
useMutation + invalidateQueries
Le pattern CRUD complet
Live coding
Transformer un fetch manuel en TanStack Query
Pourquoi c'est un problème différent
Le state serveur a des contraintes uniques
Qui ne s'appliquent pas au state client
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
| 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
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
useQueryFetching déclaratif sans boilerplate
Une seule fonction : 3 états gérés
data, isLoading, isError
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
⏳
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)
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
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
enabledconst { 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
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
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
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
const { data } = useQuery({
queryKey: todoKeys.list('done'),
queryFn: () => fetchTodos({ status: 'done' })
})
Avantages :
staleTime vs gcTime — la distinction critique
TanStack Query cache automatiquement les données
Mais quand refetch ? Quand supprimer ?
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
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000 // 5 minutes
})
Pendant 5 minutes : pas de refetch, même si :
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
gcTime: 10 * 60 * 1000 // 10 minutes
})
Scénario :
Fetch
Stale
Garbage Collected
Frais
Utilise le cache
Stale
Refetch en background
Supprimé
Nouveau fetch
useMutationPour les opérations d'écriture (POST, PUT, DELETE)
Créer, modifier, supprimer — pas de cache automatique
Mais on peut invalider les queries existantes
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é
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()
invalidateQueriesLe pattern standard après une mutation
Invalider = marquer comme stale
Déclenche un refetch automatique si la query est active
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
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>
)
}
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
onMutate
Annuler les queries en cours + sauvegarder l'ancien state + mettre à jour optimistement
onError
Restaurer l'ancien state (rollback)
onSettled
Invalider pour refetch les vraies données
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 }
},
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
Explorer le cache en temps réel
Extension navigateur + composant intégré
Indispensable pour le debugging
🔍 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} />
QueryClientProvider dans le tree React
Sans ça, rien ne fonctionne
Erreur : "No QueryClient set"
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>
)
Les erreurs à éviter absolument
Erreur classique :
Error: No QueryClient set, use QueryClientProvider to set one
✅ Solution : Envelopper l'app avec QueryClientProvider
❌ 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
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'] })
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é)
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
Documentation officielle
https://tanstack.com/query/latest
TkDodo's Blog
https://tkdodo.eu/blog/practical-react-query
React Query DevTools
Extension Chrome/Firefox
Questions ? 🤔