Guide détaillé de TanStack Query : requêtes, cache, mutations et interface utilisateur optimiste
TanStack Query gère l’état du serveur comme un gestionnaire de restaurant : clés de cache partagées, réglages de fraîcheur, mutations coordonnées, invalidation et mises à jour optimistes au-dessus de votre client HTTP.
Le sujet d’aujourd’hui est TanStack Query (anciennement React Query) : la bibliothèque qui transforme la récupération chaotique des données serveur en processus prévisibles de mise en cache, de fraîcheur et de mutation. Une analogie avec un restaurant haut de gamme rend ces mécanismes plus mémorables — la salle à manger représente l’interface utilisateur, la cuisine le backend, les serveurs les clients HTTP, et le manager TanStack Query.
Qu’est-ce que TanStack Query ?
Dans une application typique, l’interface utilisateur demande des données au backend à l’aide de fetch ou d’Axios. Ces clients ne sont pas très doués pour la coordination. Si cinq composants demandent le même menu en même temps, cela peut entraîner cinq allers-retours séparés vers la cuisine. Si un client demande la soupe du jour et qu’un autre le fait dix secondes plus tard, un serveur naïf doit retourner à la cuisine. Cela surcharge le serveur et ralentit le service.
TanStack Query agit comme le maître d’hôtel et le gérant du restaurant : il organise la récupération, le stockage en cache, la synchronisation et la mise à jour de l’état du serveur, afin que la cuisine ne soit pas sollicitée avec des questions identiques.
Axios / Fetch vs. TanStack Query
Les débutants pensent souvent que TanStack Query remplace Axios ou fetch. Ce n’est pas le cas.
- Fetch et Axios sont les serveurs. Ils transmettent une demande à la cuisine et rapportent une réponse. Ils ne se souviennent pas des demandes précédentes, n’évaluent pas la fraîcheur des données et ne coordonnent pas leurs actions avec les autres.
- TanStack Query est le gérant. Il emploie des serveurs pour effectuer les tâches. Il se souvient de ce qui a été retourné, détermine si les données sont encore considérées comme fraîches, identifie quels tables partagent le même carnet de notes, et décide quand envoyer quelqu’un chercher de nouvelles informations après que la cuisine ait modifié un plat.
queryFn qui appellent Axios ou fetch. TanStack Query enrobe ces appels avec des clés de cache, un système de déduplication, des tentatives de récupération et des hooks liés au cycle de vie.
Qu’est-ce que TanStack Query ? (carte des fonctionnalités)
Six fonctionnalités sont les plus importantes dans le travail quotidien avec React :
1. Les requêtes (useQuery) : obtenir les données
Lectures déclaratives identifiées par une queryKey, exécutées via une queryFn.
2. Le cache : le cerveau du gestionnaire
Les résultats sont stockés en mémoire selon la clé de requête, permettant ainsi à plusieurs composants d’utiliser une seule requête réseau.
3. La fraîcheur : staleTime vs gcTime
staleTime détermine quand les données en cache sont considérées comme suffisamment obsolètes pour être récupérées à nouveau. gcTime (collecte de déchets) définit la durée pendant laquelle les entrées de cache inutilisées restent avant d’être supprimées.
4. Mutations (useMutation) : modification des données
Les écritures — création, mise à jour, suppression — s’exécutent sur demande plutôt qu’au chargement du composant.
5. Invalidation des requêtes : suppression du menu
Après une mutation réussie, on marque les requêtes associées comme obsolètes afin que l’interface se synchronise à nouveau avec le serveur.
6. Mises à jour optimistes : l’expérience équivalente à une étoile Michelin
Mettre à jour le cache immédiatement, revenir en arrière en cas d’erreur, puis effectuer une synchronisation définitive.
Le reste de ce guide explique chaque fonctionnalité à l’aide d’exemples tirés du secteur de la restauration et de codes concrets.
1. Requêtes (useQuery) : récupération du menu
Une requête définit ce que l’on souhaite obtenir et la manière de le faire :
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// The waiter function (Axios)
const fetchMenu = async () => {
const response = await axios.get('/api/menu');
return response.data;
};
function MenuComponent() {
// The Manager (TanStack Query) orchestrating the process
const { data: menu, isLoading, isError, error } = useQuery({
queryKey: ['menu'], // The label for this specific data
queryFn: fetchMenu, // The waiter doing the fetching
});
if (isLoading) return <div>Waiter is walking to the kitchen... Loading Menu...</div>;
if (isError) return <div>The kitchen is on fire! Error: {error.message}</div>;
return (
<ul>
{menu.map((item) => (
<li key={item.id}>{item.name} - ${item.price}</li>
))}
</ul>
);
}
Le queryKey correspond au nom du fichier dans le carnet du gestionnaire — ['menu'], ['soup'], ['allergies', tableId]. Les clés identiques partagent le cache et éliminent les doublons dans les requêtes en cours. Le queryFn définit la procédure à suivre : il doit retourner une promesse contenant les données.
Pendant que la première requête est en cours, les indicateurs isPending / loading permettent à l’interface d’afficher des éléments de remplacement. Les erreurs sont signalées via isError et error. Les données réussies apparaissent dans la variable data et restent accessibles à tous les composants qui surveillent cette clé.
Qu’est-ce qu’une condition de course ?
L’analogie du restaurant
Imaginons deux clients qui demandent de la soupe tandis que la cuisine travaille lentement. Une réponse tardive pour la table A ne doit pas écraser la demande plus récente de la table B. Sans coordination, c’est la promesse qui se termine en dernier qui l’emporte — même si ses données sont obsolètes.
Comment cela se produit dans React (useEffect)
La récupération manuelle via useEffect oublie souvent la logique d’annulation. En naviguant rapidement entre les pages, une réponse plus ancienne peut modifier l’état après le démarrage d’une nouvelle requête.
Comment TanStack Query résout ce problème
Cette bibliothèque suive les requêtes en cours par clé, peut les annuler à l’aide de AbortSignal lorsque c’est possible, et garantit que les observateurs de l’interface voient des transitions de cache cohérentes plutôt que des conflits aléatoires dus à des appels à setState.
2. Le cache : le carnet de notes du gestionnaire
// Waiter function
const fetchMenu = async () => {
console.log("Waiter is walking to the kitchen!"); // We can track how many times this runs
const response = await axios.get('/api/menu');
return response.data;
};
// Component 1: The Sidebar
function MenuSidebar() {
const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
return <div>We have {data?.length} items today!</div>;
}
// Component 2: The Main Display
function MenuMainDisplay() {
const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
return <div>{data?.map(item => <p>{item.name}</p>)}</div>;
}
Lorsque le premier composant est chargé avec ['menu'], le gestionnaire envoie un agent de surveillance. Lorsqu’un deuxième composant est chargé avec la même clé quelques millisecondes plus tard, il lit directement le carnet de notes au lieu de réitérer la recherche. C’est cette déduplication qui permet aux tableaux de bord contenant de nombreuses cartes partageant des requêtes utilisateur ou de configuration de fonctionner rapidement, sans avoir recours à des stores globaux personnalisés.
Le gestionnaire en action (étape par étape)
- Le composant A est chargé → absence dans le cache → appel à réseau.
- La réponse arrive → écriture dans le cache → affichage du composant A.
- Le composant B est chargé avec la même clé → présence dans le cache → affichage immédiat du composant B.
- Conformément aux règles de fraîcheur, un rechargement en arrière-plan peut avoir lieu ultérieurement sans bloquer le premier affichage du composant B.
L’état du serveur doit être géré par TanStack Query ; l’état réel de l’interface utilisateur côté client (modal ouvert, onglet sélectionné) peut être conservé dans l’état de React ou dans un stockage léger côté client.
3. Fraîcheur : configuration de staleTime et gcTime
1. staleTime : ces informations sont-elles encore exactes ?
const { data } = useQuery({
queryKey: ['soup'],
queryFn: fetchSoup,
staleTime: 1000 * 60 * 30, // 30 minutes
});
Avec staleTime: 10_000, les données ayant moins de dix secondes sont considérées comme fraîches : lors d’un remontage, elles sont réutilisées sans rechargement. Une fois devenues obsolètes, les observateurs peuvent déclencher un rechargement en arrière-plan (lors du remontage, du gain de focus de la fenêtre ou de la réconnexion — en fonction des paramètres par défaut et des options). Choisissez staleTime en fonction de la volatilité du domaine : les listes de plats du jour peuvent être courtes, tandis que celles des pays le sont moins.
2. gcTime : puis-je jeter ce document ?
const { data } = useQuery({
queryKey: ['allergies', 'table4'],
queryFn: fetchAllergies,
gcTime: 1000 * 60 * 60 * 24, // Keep in memory for 24 hours
});
gcTime détermine la durée pendant laquelle une entrée de cache reste en mémoire après que tous les observateurs se sont désinscrits. Une valeur courte pour gcTime libère plus rapidement la mémoire ; une valeur plus longue rend les navigations ultérieures instantanées. Ne le confondez pas avec staleTime : les données obsolètes peuvent rester en mémoire jusqu’à ce que le collecteur de déchets les traite.
Le secret ultime : stale-while-revalidate
TanStack Query affiche volontiers des données obsolètes pendant que le rechargement a lieu en arrière-plan. Les utilisateurs voient immédiatement le dernier menu connu ; lorsque la cuisine confirme les mises à jour, le carnet s’actualise. C’est ce schéma qui fait que la bibliothèque semble plus rapide que les indicateurs de chargement à chaque visite.
4. Mutations (useMutation) : ajouter un nouveau plat
Qu’est-ce qu’une mutation ?
Une mutation modifie l’état du serveur — passer une commande, modifier un profil, supprimer un commentaire.
L’analogie du restaurant : passer une commande
Les serveurs ne passent pas automatiquement les commandes dès qu’un client s’assoit ; ils attendent une demande explicite. Il en va de même pour les mutations : elles s’exécutent lorsque vous appelez mutate ou mutateAsync.
Le code : création du formulaire de commande
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
import { useState } from 'react';
// 1. The Waiter Function (The actual network request)
const placeOrder = async (orderData) => {
// We are using POST because we are creating a new order
const response = await axios.post('/api/orders', orderData);
return response.data;
};
function OrderForm() {
const [dish, setDish] = useState('');
// 2. The Manager orchestrating the mutation
const mutation = useMutation({
mutationFn: placeOrder,
// We can also trigger side effects right here!
onSuccess: (data) => {
console.log("Chef says: Order confirmed!", data);
},
onError: (error) => {
console.log("Chef says: We have a problem.", error.message);
}
});
const handleSubmit = (e) => {
e.preventDefault();
// 3. Triggering the mutation and passing the variables
mutation.mutate({ dishName: dish, tableNumber: 4 });
};
return (
<form onSubmit={handleSubmit}>
<input
value={dish}
onChange={(e) => setDish(e.target.value)}
placeholder="What would you like?"
/>
{/* Notice how we use isPending to disable the button so they don't double-order! */}
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Sending to Kitchen...' : 'Place Order'}
</button>
{/* Handling the feedback */}
{mutation.isError && <p style={{ color: 'red' }}>Failed: {mutation.error.message}</p>}
{mutation.isSuccess && <p style={{ color: 'green' }}>Order placed successfully!</p>}
</form>
);
}
Connectez la fonction onSuccess pour afficher des messages de confirmation, des indications de navigation ou signaler une invalidation. Utilisez mutateAsync lorsque vous devez attendre la fin de l’opération dans les gestionnaires de soumission.
Détails avancés pour les développeurs
1. Elle ne s’exécute pas automatiquement
Contrairement aux requêtes, les mutations restent inactives jusqu’à ce qu’elles soient appelées—ce qui empêche des écritures accidentelles lors de la rendu.
2. isPending vs isLoading
Dans la version v5, préférez isPending pour indiquer l’état en cours de traitement d’une mutation. Alignez l’activation/désactivation de l’interface avec ce flag.
3. Éviter le problème des clics doubles
Désactivez le bouton de soumission tant que isPending est à true afin d’empêcher les utilisateurs de passer des commandes dupliquées.
5. Invalidation des requêtes : indiquer au gestionnaire de mettre à jour le carnet
Le problème : le carnet obsolète
Lorsqu’un chef ajoute un plat, les tables qui affichent encore des menus en mémoire tampon voient la liste d’hier jusqu’à ce que quelque chose force un nouveau chargement.
La solution : l’invalidation des requêtes
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function AddDishForm() {
// 1. Get access to the Manager's office
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newDish) => {
const response = await axios.post('/api/menu', newDish);
return response.data;
},
// 2. The magic happens HERE in the onSuccess callback
onSuccess: () => {
// 3. Tell the Manager to rip up the menu notepad
queryClient.invalidateQueries({ queryKey: ['menu'] });
console.log("Menu invalidated! The Manager is getting a fresh copy.");
},
});
// ... form code
}
invalidateQueries marque les entrées correspondantes comme obsolètes et déclenche un nouveau chargement pour les observateurs actifs.
Que se passe-t-il exactement lorsque vous appelez invalidateQueries ?
Les requêtes correspondantes deviennent obsolètes ; les observateurs enregistrés effectuent un nouveau chargement ; les entrées non enregistrées attendent jusqu’au prochain enregistrement (sous réserve de gcTime). La cuisine reste la source de vérité ; le carnet est alors instruit de se mettre à jour.
Le concept avancé : la correspondance floue
Invalidation ciblée :
queryClient.invalidateQueries({ queryKey: ['menu', 'lunch'] });
Ou invalider tout un préfixe :
// This rips up the breakfast, lunch, and dinner notepads all at once!
queryClient.invalidateQueries({ queryKey: ['menu'] });
La correspondance de préfixes flous perturbe les carnets de notes relatifs au petit-déjeuner, au déjeuner et au dîner lorsque ['menu'] est invalidé de manière large — puissant mais dangereux. Préférez la clé la plus restreinte qui permet de conserver une interface utilisateur correcte.
La règle d’or des mutations
Toute écriture réussie doit soit invalider les lectures qui en dépendent, soit mettre à jour le cache de manière ciblée. Laisser les lectures intactes est la cause des erreurs visuelles après enregistrement.
6. Mises à jour optimistes : l’expérience étoilée
L’analogie : la confiance du manager
Un manager de confiance peut noter la nouvelle bière sur la note avant que la cuisine ne le confirme — puis l’effacer si la tireuse est vide.
Les trois piliers d’une mise à jour optimiste
Dans useMutation :
onMutate: arrêter les requêtes conflictuelles, prendre un snapshot du cache, écrire immédiatement les données optimistes.
onError: restaurer le snapshot si le serveur refuse.onSettled: invalider (ou synchroniser d’une autre manière) afin que le cache corresponde à la base de données, que la mutation ait réussi ou échoué.Le code : ajouter un plat de manière optimiste
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function AddDishForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newDish) => {
const response = await axios.post('/api/menu', newDish);
return response.data;
},
// 1. The millisecond the user clicks submit...
onMutate: async (newDish) => {
// A. Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['menu'] });
// B. Take a snapshot of the current menu (The Eraser Backup)
const previousMenu = queryClient.getQueryData(['menu']);
// C. Optimistically update the Manager's notepad right now!
queryClient.setQueryData(['menu'], (oldMenu = []) => {
// We fake an ID for now, the real ID comes from the database later
return [...oldMenu, { ...newDish, id: Math.random().toString() }];
});
// D. Return the snapshot so onError can use it if things go wrong
return { previousMenu };
},
// 2. If the Kitchen catches on fire...
onError: (err, newDish, context) => {
// Use the eraser! Roll back to the snapshot we saved in onMutate
if (context?.previousMenu) {
queryClient.setQueryData(['menu'], context.previousMenu);
}
console.error("Chef says no! Rolling back.", err);
},
// 3. Always run this at the very end, success or fail...
onSettled: () => {
// Tell the Manager to get the real, final menu from the database
queryClient.invalidateQueries({ queryKey: ['menu'] });
},
});
// ... form code
}
Faites attention à l’annulation des requêtes en cours, à la structure du snapshot et aux chemins de rollback. Une interface utilisateur optimiste donne l’impression d’une réponse instantanée, mais elle ne doit en aucun cas laisser le cache bloqué lorsque la cuisine refuse.
Assembler les piliers fondamentaux
- TanStack Query est un gestionnaire d’état asynchrone, pas un outil de récupération de données. Il entoure Axios/
fetchpour rendre le chaos réseau prévisible. - La clé de la requête est essentielle. Elle permet la déduplication, le stockage en cache et le partage entre composants.
invalidateQueries ou des mises à jour optimistes soigneusement conçues afin que le carnet du client corresponde à la situation réelle.Paramètres par défaut pratiques pour des applications réelles
Commencez avec une valeur raisonnable de staleTime pour des ressources principalement statiques (en minutes) et une valeur courte ou nulle pour les données volatiles spécifiques à l’utilisateur. Gardez queryFn simple et pouvant être interrompu. Centralisez les clés dans des fonctions de fabrication (menuKeys.list(), menuKeys.detail(id)) afin que l’invalidation reste type-safe et cohérente. Enregistrez les événements de cache en phase de développement pour diagnostiquer les requêtes dupliquées. Préférez l’invalidation à des manipulations manuelles complexes du cache tant qu’une interface n’a pas réellement besoin d’approches optimistes.
Modèles de défaillance courants
- L’utilisation de clés instables (de nouveaux objets littéraux à chaque rendu) détruit le cache.
- Oublier de valider les données après des mutations affiche des informations fantômes.
- Fixer une valeur infinie pour
staleTimesans stratégie de mutation bloque l’interface utilisateur. - Mettre tous les indicateurs UI côté client dans le cache de requêtes brouille les limites de l’état du serveur.
- Une invalidation trop large (comme les erreurs de type
queryKey: ['']) force un rechargement complet des données.
Évitez ces problèmes, et le restaurant fonctionnera correctement : les serveurs pourront se déplacer quand nécessaire, le carnet du manager restera cohérent, et les clients verront de la nourriture chaude sans devoir surveiller la porte de la cuisine toutes les dix secondes.
Conclusion
TanStack Query se distingue en gérant les cycles de vie liés à l’état du serveur — lectures, fraîcheur des données, écritures et synchronisation — tout en laissant le transport à Axios ou fetch. Découvrez les paramètres de base (les clés), les réglages de fraîcheur (staleTime, gcTime) ainsi que les procédures d’écriture (mutations, invalidation, mises à jour optimistes). Grâce à cela, les applications React cessent de réinventer leurs caches de requêtes dans chaque useEffect et commencent à fonctionner comme un restaurant bien géré.
Pourquoi la métaphore du restaurant continue de fonctionner
Les cascades de réseau semblent abstraites tant que l’on ne s’imagine pas cinq serveurs courant vers la même soupe. La déduplication, c’est le manager qui lève la main : une seule fois qu’il se rend à la table, plusieurs clients sont servis. La stratégie « stale-while-revalidate » consiste à servir le dernier menu imprimé pendant qu’un employé vérifie le tableau des plats. L’invalidation, c’est arracher les pages lorsque le chef modifie ses recettes. Les mises à jour optimistes, c’est écrire la commande du client sur la note avant confirmation — avec un gomme à portée de main. Lors de l’intégration des nouveaux employés, il faut leur faire visualiser ces scénarios avant de leur présenter les génériques de TypeScript ; ainsi, la compréhension reste plus forte.
Intégration avec les routeurs et les mécanismes d’authentification
Lorsque les données ne sont pas globales, les clés doivent inclure l’identité du locataire ou de l’utilisateur : ['menu', restaurantId] ou ['allergies', userId]. Lors de la déconnexion, videz le cache pour éviter que les pages du carnet ne soient partagées entre différents utilisateurs. Avec React Router ou des outils similaires, déclenchez l’invalidation uniquement pour les actions qui connaissent déjà quels ressources ont changé, plutôt que de récupérer tout le contenu à chaque navigation.
Stratégies de test
Testez les mappeurs queryFn de manière indépendante en tests unitaires. Dans les tests de composants, enveloppez-les avec QueryClientProvider en utilisant un client frais et en définissant retry: false pour assurer la déterminisme. Vérifiez que les mutations appellent invalidateQueries avec les clés attendues. Pour les parcours optimistes, simulez des erreurs serveur et assurez-vous que le système revient à l’état initial. Évitez d’utiliser un seul QueryClient dans des tests non liés sans le réinitialiser.
Remarques sur les performances
Les listes longues doivent être gérées via de la pagination ou des requêtes infinies plutôt qu’avec une seule clé majeure. Les sélecteurs (select) permettent aux composants d’observer des parties spécifiques sans re-renderiser en fonction de champs de cache non pertinents. Assurez-vous que les résultats de queryFn soient serialisables et stables. Mesurez les pics de requêtes de récupération lorsque refetchOnWindowFocus est associé à une valeur très courte pour staleTime sur des tableaux de bord chargés — ajustez les paramètres par requête plutôt que de manière globale.
Mentalité de migration depuis useEffect brut
Remplacez les effets de montage qui définissent des trios chargement/erreur/données par useQuery. Remplacez les gestionnaires POST impératifs par useMutation. Supprimez les caches personnalisés. Conservez des instances d’Axios pour les intercepteurs et les en-têtes d’autorisation ; transmettez-les à queryFn. La migration se fait progressivement : traiter écran par écran réduit immédiatement le nombre d’erreurs liées aux concurrences.
Liste de contrôle finale avant déploiement d’une fonctionnalité
queryKeystable et hiérarchisé.staleTimeexplicite défini pour le domaine.- Mutation associée à une invalidation ou à une approche optimiste.
- L’interface en attente empêche les soumissions dupliquées.
- Avis d’erreur ou mécanismes de limitation bien configurés.
- Les clés liées à l’authentification sont effacées à la fin de la session.
En respectant ces six points, TanStack Query cesse d’être « une simple bibliothèque » pour devenir le gestionnaire discret dont votre interface avait besoin.
Démonstration : chargement d’un menu à travers trois composants
Imaginez un en-tête affichant la soupe du jour, une barre latérale listant les spécialités du déjeuner, et une zone principale affichant le menu complet. Sans TanStack Query, chaque zone pourrait exécuter son propre useEffect et appeler /api/menu. Avec une clé de requête partagée queryKey: ['menu', restaurantId], la première chargement effectue la requête réseau ; les autres lisent simplement le cache. Lorsque le chef modifie la soupe via un formulaire d’administration en utilisant useMutation, l’invalidation de ['menu', restaurantId] met à jour toutes les zones encore affichées. Les clients ne voient jamais trois versions différentes de la soupe à cause d’un désaccord entre les serveurs.
Cette architecture unique intègre la suppression des doublons, le cache partagé, les mutations et l’invalidation. La plupart des interfaces de production ne sont que des variantes de celle-ci : en-tête de profil accompagné d’un formulaire de paramètres, icône du panier associée aux articles de la caisse, cloche de notification jointe à la page des notifications.
Concevoir des clés de requête comme des chemins de fichiers
Traitez les clés comme des chemins hiérarchiques :
['menu', restaurantId]['menu', restaurantId, 'lunch']['menu', restaurantId, 'item', itemId]['allergies', restaurantId, tableId]
Les factories aident :
L’invalidation de ['menu', restaurantId] peut permettre une correspondance floue avec des clés plus profondes si cela est configuré, ce qui permet de conserver ensemble les listes d’actualisations et les vues détaillées. Évitez d’incorporer des valeurs non serialisables (fonctions, instances de classes) à l’intérieur des clés. Préférez des identifiants primitifs et des enums stables.
Choisir staleTime en fonction de la langue du produit
Demandez aux responsables de produit à quel point l’interface utilisateur peut être erronée pendant N secondes. Les textes de marketing qui changent chaque mois peuvent supporter une longue durée de validité. Les comptages d’inventaire lors des ventes flash nécessitent un staleTime proche de zéro, ainsi qu’une invalidation à chaque modification d’achat. Documentez ce choix à côté de la requête afin que les futurs éditeurs ne « optimisent » pas une requête volatile en créant une fenêtre de validité trop longue.
gcTime est un paramètre de gestion de la mémoire. Les applications mobiles disposant de nombreuses routes tirent parti du fait de conserver temporairement les écrans récents pour que la navigation arrière paraisse instantanée. Les caches extrêmement volumineux sur des appareils à faible mémoire exigent un gcTime plus court ou une pagination.
Mutations perçues comme sûres
Affichez toujours les états en attente et d’erreur. Désactivez les boutons destructeurs tant que l’opération est en attente. Pour les suppressions, une suppression optimiste devrait restaurer le snapshot si le serveur renvoie 409 ou 500. Pour les créations, les lignes générées de manière optimiste nécessitent que les IDs clients temporaires soient remplacés par des IDs serveur en cas de succès — ou bien évitez l’approche optimiste et invalidez plutôt les données lorsque la mise en correspondance des IDs est complexe.
Les mutations parallèles sur la même clé peuvent entrer en conflit ; mettez-les en file d’attente ou désactivez les contrôles associés. La fonction mutateAsync dans les bibliothèques de formulaires doit être intégrée aux gestionnaires d’envoi avec des blocs try/catch, et non lors de la rendu.
Modèles d’invalidation évolutifs
Après connexion, invalidez les clés liées à l’utilisateur plutôt que de vider tout le client si le contenu public doit rester affiché. Après déconnexion, queryClient.clear() est généralement la solution appropriée. Lorsqu’un WebSocket annonce un « changement de menu », appelez les mêmes outils d’invalidation utilisés par la mutation HTTP afin que les deux méthodes partagent une même stratégie de synchronisation.
Faites un préchargement au survol pour les pages de détails probables : queryClient.prefetchQuery({ queryKey, queryFn }) transforme la latence perçue en accès au cache sans modifier le code affiché à l’écran.
Mises à jour optimistes sans mythes
L’approche optimiste n’est pas obligatoire pour chaque requête POST. Utilisez-la lorsque le scénario idéal est fréquent, que l’avantage pour l’interface utilisateur est évident et que le retrait des modifications est facile à mettre en œuvre. Évitez-la lorsque la validation serveur est complexe ou lorsque le corps de la réponse est nécessaire à l’affichage (identifiants générés par le serveur, prix, taxes). Un indicateur de chargement lent peut être préférable à une affichage rapide de données incorrectes.
Lorsque vous utilisez l’optimisme, gardez les snapshots immuables, annulez les requêtes conflictuelles dans onMutate, et synchronisez toujours dans onSettled. Enregistrez les annulations en mode développement ; des annulations silencieuses confondent le service de qualité.
Comparaison avec les stores clients globaux
Redux ou Zustand peuvent stocker des données serveur, mais vous devrez recréer les caches, demander la déduplication et effectuer des mises à jour en arrière-plan. TanStack Query se spécialise dans ce domaine. Conservez l’UI éphémère dans l’état local ou un petit store client ; conservez les entités serveur dans le cache de requêtes. Mélanger ces flux entraîne des sources de vérité dupliquées.
Formation de l’équipe
Organisez un atelier : créez une petite application de menu avec une requête de liste, une requête de détail, une mutation de création, une invalidation, puis une création optimiste. Exigez l’utilisation de fonctions générateurs de clés ainsi qu’un mécanisme de déconnexion. Une fois ce schéma intégré comme une habitude, les applications plus complexes cessent d’accumuler des erreurs liées aux useEffect fetch.
Tableau récapitulatif en prose
Les requêtes servent à lire. Les mutations servent à écrire. Les clés désignent les lignes du cache. staleTime répond à la question : « Puis-je réutiliser sans demander à la cuisine ? » gcTime répond : « Puis-je jeter cette page de carnet ? » L’invalidation indique que « la cuisine a changé — mettez à jour ». L’optimisme signifie : « Mettez à jour la facture maintenant, effacez-la si elle est rejetée ». Le transport se fait via Axios ou fetch. Cette répartition des tâches constitue l’ensemble du produit.
Scénario bout en bout : tableau des spécialités déjeuner
Un restaurant commence son service déjeuner. La requête affichant les spécialités utilise queryKey: ['menu', restaurantId, 'lunch'] avec un délai de péremption de deux minutes, car les tableaux noirs sont modifiés lentement au cours d’un service. Le widget dédié aux soupes utilise ['menu', restaurantId, 'soup'] avec un délai de péremption de trente secondes. Les deux fonctions queryFn appellent la même instance Axios munie d’intercepteurs d’autorisation. Lorsqu’un administrateur enregistre une nouvelle soupe via useMutation, la fonction onSuccess invalide les deux clés – ou bien le préfixe commun ['menu', restaurantId] si une recherche approximative est prévue. Les clients sur chaque tablette active voient les mises à jour sans avoir besoin de recharger manuellement.
Si le formulaire d’administration utilisait une fonction fetch personnalisée sans mécanisme d’invalidation, les tablettes afficheraient des données obsolètes jusqu’à ce qu’elles soient rechargées. C’est précisément ce problème que TanStack Query vise à éviter.
Fabriques de clés de requête en TypeScript
Centraliser les clés :
menuKeys.all(restaurantId)menuKeys.lunch(restaurantId)menuKeys.item(restaurantId, itemId)
Les factories empêchent les fautes de frappe et permettent une recherche efficace des éléments invalidés dans le code. Préférez les tuples de types primitifs. Lorsque des filtres existent, incluez des objets de filtre serialisés avec un ordre de clés stable. Ne mettez jamais tout l’objet des options provenant de props dans la clé, à moins qu’il ne soit mémorisé et serialisable.
Options de useQuery que vous utiliserez réellement
Au-delà de queryKey et queryFn : enabled empêche les requêtes tant que les IDs n’existent pas ; retry gère la politique de gestion des pannes temporaires ; refetchOnWindowFocus peut être désactivé pour les tableaux de bord coûteux ; placeholderData ou initialData maintiennent la stabilité des layouts ; select restreint les données abonnées afin de réduire les rechargements. Les valeurs par défaut conviennent pour commencer ; ajustez-les selon chaque requête lorsque les profils montrent des pics de rechargements.
Comprendre le concept de « stale-while-revalidate » en termes d’expérience utilisateur
Afficher le total du panier d’hier pendant 100 ms pendant un rechargement peut être inacceptable ; afficher l’article du centre d’aide d’hier pendant une minute est acceptable. Définissez cela dans staleTime, et non à l’aide de flags ad hoc. Une panne de rechargement en arrière-plan ne doit pas effacer des données obsolètes fiables sauf si vous le choisissez explicitement ; les utilisateurs préfèrent des informations légèrement désuètes plutôt qu’un message d’erreur avec un indicateur de chargement en mode hors ligne.
Mutations : anatomie d’une soumission de forme solide
Désactivez le bouton en attente ; affichez les erreurs inline provenant de error ; en cas de succès, invalidez ou mettez à jour le cache ; en cas de règlement, effacez l’état local du formulaire si nécessaire. Utilisez mutateAsync avec try/catch à l’intérieur du gestionnaire de soumission de la bibliothèque de formulaire. Ne appelez pas mutate en boucle sans contrôle de concurrence. Pour les téléchargements, affichez séparément l’avancement — TanStack Query suit l’état de la mutation, pas l’avancement en octets.
Histoires de granularité d’invalidation
Trop restreint : mise à jour de la liste mais oubli des détails → page de détail obsolète. Trop large : chaque clé sous ['menu'] est rechargée → surcharge importante. Adaptez la granularité aux écrans capables d’afficher des incohérences. En cas de doute, invalidez la liste ainsi que l’ID des détails que vous avez modifiés. Le préfixe d’invalidation sert à une diffusion intentionnelle.
Péchés des mises à jour optimistes
Le snapshot doit effectuer un clonage profond suffisant de la structure pour restaurer les listes imbriquées. Les identifiants temporaires des clients ne doivent pas s’échapper vers le serveur. Lorsque plusieurs mutations optimistes se superposent, les annulations peuvent s’annuler mutuellement — serialisez l’interface utilisateur pour ces flux. Vérifiez toujours la cohérence avec les données réelles du serveur même en cas de succès, car le serveur peut normaliser des champs que vous n’avez pas envoyés.
Mode strict de React et montage double
Pendant le développement, le Mode strict exécute les effets deux fois. TanStack Query élimine les doublons en se basant sur une clé, de sorte que vous ne devriez pas voir deux appels réseau pour la même clé en cours d’exécution. Si c’est le cas, votre clé est instable ou des problèmes d’identité de queryFn viennent contredire les hypothèses de suppression des doublons. Journalisez les clés lors du débogage.
Notes sur le SSR et l’hydratation
Pour Next.js et des solutions similaires, déshydratez le client de requêtes sur le serveur et hydratez-le côté client afin que le bloc-notes survive aux navigations. Assurez-vous que les fonctions queryFn s’exécutent dans les deux environnements, ou fournissez un préchargement côté serveur qui remplira le cache avant l’affichage. Des structures de données incompatibles entre le serveur et le client génèrent des avertissements d’hydratation qui ressemblent à des bugs du framework, mais sont en réalité dus à des problèmes de préchargement du cache.
Comparaison avec les patterns SWR personnalisés
De nombreuses équipes réinventent des sous-ensembles de cette bibliothèque : cartes de cache, préchargement ciblé, modification suivie de révalidation. TanStack Query standardise ces approches grâce à des paramètres par défaut communautaires et aux outils de développement. La création manuelle n’est justifiée que pour de très petits applications ou des environnements d’exécution particuliers. Sinon, l’approche basée sur des outils standard gagne en termes d’économie de temps.
Outils de développement et observabilité
Les outils de développement de React Query affichent les clés, le degré de péremption, les observateurs et l’état des requêtes. Apprenez à votre équipe à les interpréter avant d’ajouter des messages dans la console. En environnement de production, supprimez les données sensibles des rapports d’erreurs ; en cas de nécessité liée à la confidentialité, enregistrez les échecs des requêtes avec les noms des clés et non avec l’intégralité des données transmises.
Liste de contrôle des anti-modèles
Clés instables ; absence d’invalidation ; état de péremption infini sans synchronisation des mutations ; insertion de flags UI dans le cache serveur ; invalidation trop large ; mises à jour optimistes sans possibilité de réversion ; ignorer l’attribut enabled tant que les identifiants n’existent pas ; utiliser des mutations pour des opérations de lecture. Évitez ces pratiques pour maintenir l’ordre.
Résumé pour ceux qui lisent rapidement
Les serveurs assurent le transport. Les gestionnaires se souviennent et coordonnent. Les clés désignent les pages du carnet de notes. Des curseurs de fraîcheur contrôlent la réutilisation. Les mutations enregistrent les modifications. L’invalidation et l’optimisme maintiennent l’honnêteté du carnet de notes. Voilà TanStack Query en une phrase — et c’est pourquoi il entoure Axios plutôt que de le remplacer.
Exercices supplémentaires pour s’entraîner en cuisine
Réconstruisez une petite application : requête de liste, requête de détail, création de mutation avec invalidation, puis création optimiste avec chemin d’erreur forcé. Ajoutez une fonction de déconnexion qui efface les données côté client. Ajoutez un préchargement au survol d’un élément de liste. Mesurez les appels réseau dans Devtools avant et après utilisation des clés partagées. Ces exercices permettent de maîtriser la bibliothèque mieux que la simple lecture des tableaux d’API.
Lors de l’examen des PR, demandez-vous : quelle est la clé ? Qu’est-ce que staleTime et pourquoi l’utiliser ? Qu’est-ce qui provoque l’invalidation après une écriture ? L’interface en attente empêche-t-elle les soumissions doubles ? Si ces réponses sont claires, la fonctionnalité fonctionnera correctement en situation de concurrence et sous pression de navigation.
Modèles de préchargement qui donnent l’impression d’une réponse instantanée
Préchargez les données lorsque le curseur passe sur une route, lorsqu’un onglet devient actif avant que l’utilisateur n’appuie sur le bouton, ou après connexion pour la requête par défaut du tableau de bord. Le préchargement remplit le cache sans avoir besoin d’un observateur. Lorsque l’utilisateur navigue, useQuery trouve les données déjà en mémoire et évite l’affichage du squelette de chargement. Si vous préchargez la mauvaise clé, vous consommez inutilement de la bande passante ; si c’est la bonne clé, les performances perçues s’améliorent sans modifier le code de queryFn.
Associez le préchargement à une valeur réaliste pour staleTime. Précharger des données qui deviennent immédiatement obsolètes déclenche un nouveau chargement en arrière-plan instantané — ce qui reste mieux qu’un démarrage à froid, mais ce n’est pas gratuit. Préchargez uniquement les requêtes essentielles du chemin critique ; laissez de côté les écrans de configuration rares.
Requêtes dépendantes et contrôle en cascade
Lorsque les détails nécessitent un identifiant provenant d’une sélection dans une liste, utilisez un contrôle avec enabled: !!selectedId. Lorsqu’une deuxième requête a besoin de données provenant de la première, assurez-vous d’assembler soigneusement les éléments : soit imbriquez la deuxième clé avec l’identifiant du premier résultat, soit utilisez une seule fonction de requête qui retourne les deux formats si l’API le permet. Les séquences de requêtes successives ralentissent le temps de réponse ; les requêtes parallèles utilisant des en-têtes d’authentification partagés sont préférables lorsque cela est possible.
Le mode Suspense modifie la manière dont les limites de chargement sont organisées. Si l’équipe utilise Suspense, alignez les limites d’erreur et veillez à ce que les erreurs de requête se produisent comme prévu. Un mélange de Suspense et de flags de chargement classiques confond les auditeurs — choisissez un style par arbre de routes.
Pagination, requêtes infinies et pages en cache
Les listes de pages utilisent souvent des paramètres de page dans la clé : ['orders', { page, pageSize, status }]. Changer de page crée une nouvelle entrée dans le cache ; conservez les données précédentes avec placeholderData: keepPreviousData (ou l’équivalent actuel de l’API) afin que le tableau ne reste pas vide. Les requêtes infinies ajoutent des pages ; invalidez-les avec prudence pour ne pas effacer inopinément la position de défilement. Lorsqu’une mutation modifie une ligne, mettez à jour cette entrée de page ou invalidez toute la liste en fonction de la sensibilité au tri.
Récupération des erreurs et expérience utilisateur pour les tentatives de réessai
Les tentatives par défaut aident à gérer les réseaux mobiles instables. Pour les codes 401/403, désactivez les tentatives et redirigez vers la page de connexion. En cas de code 404 pour les détails, échouez rapidement. Affichez failureCount et failureReason dans les éléments d’aide destinés aux utilisateurs avancés. Les écouteurs globaux de QueryCache peuvent afficher un message d’erreur une seule fois par clé plutôt qu’une fois par observateur, afin d’éviter des surcharges de messages lorsqu’il y a cinq composants partageant une requête défaillante.
Stratégies de test
Dans les tests unitaires, enveloppez le code avec un nouveau QueryClient défini sur retry: false ainsi qu’avec une collecte de déchets courte. Mockez queryFn ou utilisez MSW. Vérifiez les états de chargement, de succès et d’erreur. Pour les mutations, assurez-vous que invalidateQueries a été appelé avec la clé attendue. Les tests d’intégration doivent confirmer que deux composants partageant une clé ne effectuent pas de requêtes doubles. Les tests instables proviennent souvent d’un cache non vidé entre les cas de test : créez un nouveau client pour chaque test.
Mises à jour de version et dérive API
Dans la transition de TanStack Query v4 à v5, certaines options ont été renommées et les valeurs par défaut modifiées. Lors d’une mise à niveau, consultez le guide de migration, mettez à jour le paquet Devtools et vérifiez à nouveau l’utilisation de keepPreviousData / placeholderData. Fixez les versions dans des fichiers de verrouillage. Considérez les changements dans la structure de la clé de requête comme des modifications brisantes : les caches hydratés anciens ne correspondent pas nécessairement aux nouvelles clés après déploiement — acceptez un cache froid temporaire ou versionnez le préfixe de la clé.
Notes de terrain issues d’incidents en production
Une équipe a constaté des envois POST dupliqués parce que le bouton d’envoi restait activé alors que isPending était à vrai sur une autre instance de mutation. Une autre a effacé tout le cache lors de la sortie incorrectement, en créant un nouveau client sans supprimer l’ancienne référence de fournisseur. Une troisième a encodé les objets utilisateurs dans des clés, ce qui a perturbé le partage structurel. Insérez ces cas dans les documents d’intégration afin que les nouveaux arrivants héritent de ces erreurs sans avoir à les découvrir eux-mêmes.
Dokumentez les paramètres par défaut de votre système : la valeur par défaut de staleTime, les requêtes qui sont limitées au niveau de l’utilisateur, la manière dont la sortie efface l’état, et les moments où les mises à jour optimistes sont autorisées. La cohérence l’emporte sur la ruse.
Notes de terrain issues d’incidents en production
Une équipe a constaté des envois POST dupliqués parce que le bouton d’envoi restait activé alors que isPending était à vrai sur une autre instance de mutation. Une autre a effacé tout le cache lors de la sortie incorrectement, en créant un nouveau client sans supprimer l’ancienne référence de fournisseur. Une troisième a encodé les objets utilisateurs dans des clés, ce qui a perturbé le partage structurel. Insérez ces cas dans les documents d’intégration afin que les nouveaux arrivants héritent de ces erreurs sans avoir à les découvrir eux-mêmes.
Dokumentez les paramètres par défaut de votre système : la valeur par défaut de staleTime, les requêtes qui sont limitées au niveau de l’utilisateur, la manière dont la sortie efface l’état, et les moments où les mises à jour optimistes sont autorisées. La cohérence l’emporte sur la ruse.
Notes de terrain issues d’incidents en production
Une équipe a constaté des envois POST dupliqués parce que le bouton d’envoi restait activé alors que isPending était à vrai sur une autre instance de mutation. Une autre a effacé tout le cache lors de la sortie incorrectement en créant un nouveau client sans supprimer la référence à l’ancien fournisseur. Une troisième a encodé les objets utilisateurs dans des clés, ce qui a perturbé le partage structurel. Inscrivez ces cas dans les documents d’intégration afin que les nouveaux arrivants héritent de ces erreurs sans avoir à les découvrir eux-mêmes.
Dokumentez les paramètres par défaut de votre système : la valeur par défaut de staleTime, les requêtes qui sont limitées au niveau de l’utilisateur, la manière dont la sortie efface l’état, et les moments où les mises à jour optimistes sont autorisées. La cohérence l’emporte sur la ruse.