Qu’est-ce que TanStack Query ?
TanStack Query, anciennement React Query, est une bibliothèque de gestion d’état serveur (server-state management). Elle part du principe que les données récupérées via une API sont fondamentalement différentes de l’état que vous conservez dans useState. Les données serveur sont partagées, mises en cache, peuvent devenir obsolètes et doivent être récupérées à nouveau. Gérer cela avec des effets et des drapeaux (flags) est fastidieux et source de bugs.
TanStack Query offre aux données serveur un cache approprié. Vous décrivez comment récupérer une donnée et vous lui attribuez une clé ; la bibliothèque s’occupe ensuite du caching, de la déduplication, des états de chargement et d’erreur, du rafraîchissement en arrière-plan et de l’invalidation. C’est la couche de données manquante pour React, et elle s’intègre parfaitement avec n’importe quelle bibliothèque de gestion d’état client.
Requêtes
Une requête est définie par une clé et une fonction de requête. La clé identifie l’entrée dans le cache ; la fonction récupère les données.
// api.ts
async function fetchPost(id: string) {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Post.tsx
import { useQuery } from "@tanstack/react-query";
export function Post({ id }: { id: string }) {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
});
if (isLoading) return <p>Loading…</p>;
if (isError) return <p>Error: {error.message}</p>;
return <article>{data.title}</article>;
}
Chaque composant utilisant la même clé partage la même entrée de cache, ainsi, le montage du composant deux fois ne déclenche qu’un seul appel. Les indicateurs de statut sont fournis, vous n’avez donc jamais besoin de les écrire vous-même.
Clés de requête et cache
La clé représente l’identité de l’entrée du cache et doit inclure toutes les variables dont dépend la récupération des données.
// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
queryKey: ["posts", { page, status }],
queryFn: () => fetchPosts({ page, status }),
});
Comme les clés sont des tableaux, vous pouvez invalider toute une famille de données à l’aide d’un préfixe. L’invalidation de ["posts"] invalide également ["posts", { page: 1 }], ce qui rend les mises à jour du cache prévisibles.
Fraîcheur et rafraîchissement des données
Deux options contrôlent le moment où les données sont récupérées à nouveau.
// freshness.ts
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 60_000, // fresh for one minute
gcTime: 5 * 60_000, // keep unused data for five minutes
});
staleTime définit la durée pendant laquelle les données sont considérées comme fraîches ; tant qu’elles sont fraîches, aucun rafraîchissement en arrière-plan n’a lieu. gcTime définit la durée pendant laquelle une entrée inutilisée reste en mémoire avant d’être supprimée par le garbage collector. Par défaut, les requêtes se rafraîchissent lors du montage, lors du focus de la fenêtre et lors de la reconnexion, ce qui permet de maintenir l’UI à jour sans aucune intervention manuelle.
Mutations
useMutation gère les écritures : la création, la mise à jour et la suppression.
// AddPost.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
}).then((r) => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return (
<button
onClick={() => mutation.mutate("New post")}
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving…" : "Add post"}
</button>
);
}
Après une mutation réussie, l’invalidation des requêtes affectées indique à chaque consommateur de refaire l’appel (refetch). Cela permet de garder le serveur comme source de vérité et d’éviter toute manipulation manuelle du cache.
Mises à jour optimistes
Pour les actions qui doivent paraître instantanées, mettez à jour le cache avant que le serveur ne réponde et annulez les modifications en cas d’échec.
// optimistic.ts
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ["post", postId] });
const previous = queryClient.getQueryData(["post", postId]);
queryClient.setQueryData(["post", postId], (old) => ({
...old,
likes: old.likes + 1,
}));
return { previous };
},
onError: (_err, postId, context) => {
queryClient.setQueryData(["post", postId], context.previous);
},
onSettled: (_data, _err, postId) => {
queryClient.invalidateQueries({ queryKey: ["post", postId] });
},
});
onMutate applique la modification optimiste et enregistre l’ancienne valeur, onError effectue le rollback, et onSettled relance la récupération des données pour se synchroniser avec le serveur.
Pagination et requêtes infinies
TanStack Query offre un support natif pour les données paginées et infinies. useQuery avec une option keepPreviousData rend les transitions entre les pages fluides, et useInfiniteQuery gère les listes basées sur des curseurs grâce à une fonction fetchNextPage. Comme chaque page fait partie du cache, la navigation avant et arrière est instantanée.
État serveur versus état client
C’est cette distinction qui permet de vraiment comprendre l’intérêt de TanStack Query :
- L’état serveur appartient au serveur, il est partagé, asynchrone et peut devenir obsolète. Utilisez TanStack Query.
- L’état client appartient au navigateur, il est synchrone et local. Utilisez
useState, Context, Zustand ou Redux.
Mélanger les deux dans un seul store est source de nombreuses complexités. Les séparer — TanStack Query pour les données, et un petit store pour l’état de l’UI — permet de garder chaque partie simple.
Bonnes pratiques
- Incluez chaque variable dans la query key.
- Définissez
staleTimedélibérément ; la valeur par défaut de zéro déclenche des refetchs très fréquents. - Utilisez des mutations accompagnées d’une invalidation plutôt que de mettre à jour manuellement le cache partout.
- Gardez vos fonctions de requête courtes et colocalisées avec la requête.
- Ajoutez des mises à jour optimistes uniquement là où un retour instantané est essentiel.
- Utilisez les devtools pour inspecter l’état du cache pendant le développement.
- Gardez l’état du serveur dans TanStack Query et l’état client ailleurs.
Erreurs courantes
- Utiliser la même clé pour différentes entrées, ce qui entraîne l’affichage de données obsolètes ou incorrectes.
- Laisser
staleTimeà zéro, déclenchant ainsi des refetches constants. - Dupliquer les données du serveur dans un store global.
- Oublier d’invalider le cache après une mutation, affichant ainsi des données périmées.
- Écrire des mises à jour optimistes (optimistic updates) sans prévoir de mécanisme de rollback.
- Traiter les fonctions de requête comme des composants et appeler des hooks à l’intérieur de celles-ci.
Et après ?
TanStack Query est la méthode standard pour gérer les données serveur dans les applications modernes. Associez-le à l’ Fetch API pour la couche de requêtes, à un store client comme Zustand pour l’état de l’UI, et à Redux Toolkit si vous avez besoin d’un état global strict. Pour Vue, Pinia gère l’état client tandis que TanStack Query s’occupe de la partie serveur.