Comment TanStack Query révolutionne le data-fetching ?
Pourquoi Tanstack Query ?
Problème #1 : le partage de données serveurs
Le cas typique auquel les développeurs sont souvent confronté, c'est celui ou vous devez partagés des données serveurs avec différents composants. Pour ce faire, on a 3 options :
- Faire 3 requêtes identiques (🤮)
- Remonter l'état au parent commun mais au risque de faire du prop drilling (re-🤮)
- Utiliser un state manager global
La troisième option étant celle qui préserve le mieux l'estomac des développeur, elle a été très largement adoptée. On fetch les données, on les stocke dans un stage manager / store (Redux, vuex, Pinia...) et chaque composant vient se servir. C'est d'ailleurs ce que rappelle le créateur de Tanstack Query :
Au fil des ans, j'ai remarqué que les modèles d'accès et de manipulation de nos données dans nos applications ont rapidement élu domicile dans ce nous appelons "l'état global". L'état global est super pratique. Il nous aide à éviter le "props drilling", et il nous permet d'accéder aux données de notre application sans la copier ou la dupliquer.
Tanner Linsley, “React Query: It’s Time to Break up with your Global State!”
Mais pour le créateur de Tanstack Query, cette pratique est loin d'être idéale. Toujours dans le même article, Tanner nous explique l'erreur qui s'en est suivi : la confusion entre l'état serveur et l'état client.
Nous avons menti, à nous même et à notre code, en laissant penser que tout état est égal, alors que je pense que nos données asynchrones et notre état global ne pourraient pas être plus différents, en particulier quand il s'agit de l'endroit où ils sont stockés, de la vitesse à laquelle nous y accédons, et de la façon dont nous y accédons et les mettons à jour, et finalement, sur qui peuvent y apporter des changements. [...]
Pour rendre les choses plus faciles à comprendre, je veux arrêter l'utilisation le terme état global et appeler ces deux types d'état différents : état client et état serveur.
L'état serveur à tout ce qui concerne les données stockées sur le serveur alors que l'état client correspond à tout ce qui concerne l'état interne de notre application. Pour donner un exemple concret
| État Serveur | État Client |
|---|---|
| La liste des utilisateurs | L'état d'une modal (ouvert/fermé) |
| Les détails d'un produit | Le thème de l'application (dark/light) |
| Les messages d'une conversation | L'étape actuelle d'un formulaire multi-steps |
| Le profil de l'utilisateur connecté | Les filtres sélectionnés dans une UI |
| Les commandes d'un client | L'état d'un accordion ou d'un menu déroulant |
| Les statistiques d'un dashboard | Les préférences utilisateur non persistées |
| L'état de sélection (éléments cochés dans une liste) |
L'état serveur a des caractéristiques :
- Asynchrone : nécessite une requête pour y accéder
- Persistant : existe indépendamment de votre app
- Partagé : d'autres utilisateurs/processus peuvent le modifier
- Potentiellement obsolète : votre copie peut être "stale" à tout moment
- Source de vérité distante : le serveur a toujours raison
bien différentes de celles de l'état client :
- Synchrone : disponible instantanément
- Éphémère : disparaît au refresh (sauf si persisté volontairement)
- Local : n'existe que dans cette instance de l'app
- Contrôlé : vous êtes le seul à pouvoir le modifier
L'état serveur et l'état client ont tous les deux clairement besoin beaucoup d'amour, mais chacun à sa manière.
Nos outils traditionnels (Redux, Vuex/Pinia, les stores Svelte...) sont conçus pour l'état client. Il ne gèrent rien concernant l'état des requêtes, ni la synchronisation avec les données du serveur, ni le cache, ni les retries...
Pour Tanner l'état serveur a des besoins spécifiques qui sont pour l'instant tous ignorées par ces outils.
Problème #2 : code boilerplate et approche impérative
Cette non prise en compte des spécificités de l'état serveur par les state managers laisse nos pauvres développeur dépourvus pour gérer les problématiques qui viennent avec nos données serveurs :
- le data fetching : les state managers ne s'occupent pas de la récupération des données, on doit toujours les récupérer manuellement et gérer les erreurs, les retries etc...
- La synchronisation : avec le state managers, nos données serveur possèdent 2 sources de vérité : le serveur et l'état global. On doit absolument s'assurer de leur parfaite synchronisation ;
De plus, le caractère asynchrone des données serveurs nous oblige à adopter une approche impérative du code. Or ce n'est pas vraiment la philosophie de framework js modernes…
Pour résumer non seulement l'absence d'outils spécifique pour l'état serveur nous oblige à faire beaucoup de code boilerplate, mais code doit en plus être abordé de manière impérative, ce qui est source d'erreurs et de bugs.
ℹ️ Le code boilerplate (ou "code passe-partout") désigne du code répétitif et standardisé qu'on doit écrire encore et encore pour accomplir des tâches courantes, sans qu'il apporte vraiment de valeur métier. C'est du code nécessaire mais peu intéressant.
TanStack Query : La librairie data-fetching qu’il manquait !
Comment ça fonctionne ?
Fondamentalement, Tanstack Query est un gestionnaire d’état serveur qui va utiliser une mise en cache intelligente des données. Du côté de l’application, lorsqu’on a besoin de données serveur, il nous suffit de d’appeler une Query :
import { useQuery } from '@tanstack/react-query';
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
});
if (isPending) return <span>Loading...</span>;
if (error) return <span>Oops!</span>;
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
Les super pouvoirs de Tanstack Query
1. Le "stale-while-revalidate"
Littéralement, périmé-pendant-revalidation. C'est le concept central. Quand les données sont en cache mais potentiellement obsolètes, TanStack Query :
- Affiche immédiatement les données du cache (UX instantanée ✨)
- Lance une requête en arrière-plan pour rafraîchir
- Met à jour l'UI silencieusement si les données ont changé
Résultat : vos utilisateurs voient toujours quelque chose, et les données sont toujours fraîches.
2. L'invalidation intelligente
Quand vous modifiez des données (ajout d'un todo par exemple), vous pouvez invalider le cache de manière ciblée :
En React :
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return <button onClick={() => mutation.mutate({ title: 'New todo' })}>Add</button>;
}
3. Les mises à jour optimistes
Pour une UX ultra-réactive, vous pouvez mettre à jour l'interface avant que le serveur réponde. Le principe est identique dans tous les frameworks :
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Annule les requêtes en cours
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Sauvegarde l'état précédent
const previousTodos = queryClient.getQueryData(['todos']);
// Met à jour le cache de manière optimiste
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback en cas d'erreur
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
L'interface réagit instantanément, et en cas d'erreur, on revient à l'état précédent. Vos utilisateurs vont vous adorer.
Partage des données serveur entre les composants
Avec TanStack Query ? Chaque composant fait son petit useQuery / createQuery avec la même queryKey: ['todos'] et... une seule requête est envoyée. Les autres composants récupèrent instantanément les données du cache.
// Composant A (React, Vue ou Svelte - même principe)
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Composant B (ailleurs dans l'app)
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Composant C (encore ailleurs)
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Résultat : 1 seule requête HTTP, 3 composants synchronisés
Franchement ? Elle est pas belle la vie ?
Du code verbeux au code déclaratif
Le deuxième problème que nous avions évoqué était celui était le fait que l’absence d’outils dédié nous obligeait à écrire du code boilerplate. Avec Tanstack, plus besoin d’écrire un code verbeux pour récupérer les données, les synchroniser ou les partager. Ses super pouvoirs font tout pour nous.
L'impact sur l'UX
TanStack Query, ce n'est pas juste du confort développeur. C'est une amélioration concrète de l'expérience utilisateur :
| Avant | Après |
|---|---|
| Spinners à chaque navigation | Données instantanées (cache) |
| Données potentiellement obsolètes | Sync automatique en arrière-plan |
| UI bloquée pendant les mutations | Mises à jour optimistes |
| Erreurs réseau = crash | Retries automatiques |
| Onglet inactif = données mortes | Refetch au focus |
Installation
Pour ceux qui veulent essayer, le setup est vraiment simple selon votre framework :
React :
npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<MaSuperApp />
</QueryClientProvider>
);
}
Vue.js :
npm install @tanstack/vue-query
import { VueQueryPlugin } from '@tanstack/vue-query';
const app = createApp(App);
app.use(VueQueryPlugin);
app.mount('#app');
Svelte :
npm install @tanstack/svelte-query
<!-- +layout.svelte (SvelteKit) -->
<script>
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
const queryClient = new QueryClient();
</script>
<QueryClientProvider client={queryClient}>
<slot />
</QueryClientProvider>
Et c'est parti ! Chaque composant peut maintenant utiliser les hooks/fonctions de query et mutation.
Bonus : les DevTools sont disponibles pour tous les frameworks :
# React
npm install @tanstack/react-query-devtools
# Vue
npm install @tanstack/vue-query-devtools
# Svelte
npm install @tanstack/svelte-query-devtools
Conclusion
Bien évidemment, TanStack Query demande un petit changement de mentalité au début. On passe d'un contrôle impératif ("je gère tout") à une approche déclarative ("je déclare mes besoins"). Cependant, une fois qu'on a compris le concept de queryKey et le système de cache, la prise en main est assez rapide.
Le gros avantage ? Les concepts sont identiques quel que soit le framework. Si vous maîtrisez TanStack Query en React, vous pouvez passer sur un projet Vue ou Svelte sans réapprendre quoi que ce soit. Seule la syntaxe du framework change, pas la logique.
Le fait d'avoir une séparation claire entre état client et état serveur ouvre tout un horizon pour simplifier nos applications. Par ailleurs, le cadre offert par TanStack Query (cache, invalidation, mutations) me semble opportun dans le cadre du travail en équipe. C'est un bon moyen de garder une architecture cohérente !
| 😎 Plutôt Cool | 🤨 Pas ouf |
|---|---|
✅ Réduction drastique du boilerplate (adieu le ✅ Cache intelligent et synchronisation automatique ✅ UX améliorée grâce au stale-while-revalidate ✅ Framework-agnostic : mêmes concepts React, Vue, Svelte, Solid, Angular ✅ DevTools excellents pour le debugging ✅ TypeScript-first avec une inférence de types impeccable | ... |
Pour aller plus loin, je vous recommande vivement la documentation officielle qui est excellente (et disponible pour chaque framework !), ainsi que le cours officiel sur query.gg.
Et comme dirait Kent C. Dodds : "Si React Query avait existé avant Redux, je ne pense pas que Redux aurait été aussi populaire." Et aujourd'hui, cette réflexion s'applique à tout l'écosystème frontend.
À méditer...