TanStack Query v5 (anciennement React Query) est devenu l'outil de référence pour la gestion du cache de données dans les applications React. Associé au App Router de Next.js, il permet de précharger les données côté serveur et de les hydrater instantanément côté client, sans état de chargement visible pour l'utilisateur.
Pourquoi TanStack Query plutôt que fetch natif dans Next.js
Next.js expose fetch avec des options de cache intégrées, ce qui suffit pour des pages statiques ou des layouts peu interactifs. Dès qu'on travaille sur une interface avec des mutations, des revalidations conditionnelles ou une synchronisation entre composants, les limites apparaissent : pas d'invalidation granulaire, pas de déduplication automatique des requêtes en vol, pas de gestion fine de l'état stale/fresh.
TanStack Query v5 résout ces problèmes de manière déclarative. La bibliothèque pèse environ 13 Ko gzippé sans aucune dépendance externe. Son modèle de cache repose sur deux paramètres essentiels :
| Paramètre | Valeur par défaut | Rôle |
|---|---|---|
staleTime | 0 ms | Durée pendant laquelle les données sont considérées fraîches |
gcTime | 5 minutes | Durée avant suppression des données inactives du cache |
La décision de refetch suit une logique simple. Pour une donnée fetched au temps , elle est considérée fraîche tant que :
Configurer un staleTime adapté à la fréquence de mise à jour des données est l'une des optimisations les plus directement visibles sur les performances perçues. Une liste de produits qui ne change pas souvent peut avoir un staleTime de 5 minutes. Un flux de notifications en temps réel restera à 0.
Le pattern préchargement + hydratation avec App Router
La documentation officielle de TanStack Query v5 sur le rendu serveur décrit une mécanique en trois étapes.
1. Précharger côté serveur
Dans un Server Component, on crée un QueryClient dédié à la requête entrante, on lance le préchargement, puis on sérialise l'état du cache avec dehydrate.
// app/produits/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { ProductList } from '@/components/product-list'
import { fetchProducts } from '@/lib/api'
export default async function ProduitsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['products'],
queryFn: fetchProducts,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductList />
</HydrationBoundary>
)
}
2. Consommer côté client
Le Client Component utilise useQuery avec la même queryKey. Quand il s'exécute sur le client, il trouve les données déjà présentes dans le cache injecté par HydrationBoundary. Aucun spinner, aucun refetch inutile au montage.
// components/product-list.tsx
'use client'
import { useQuery } from '@tanstack/react-query'
import { fetchProducts } from '@/lib/api'
export function ProductList() {
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 60 * 1000, // 1 minute
})
return (
<ul>
{products?.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
3. Configurer le QueryClient global côté client
Le QueryClientProvider s'initialise une seule fois côté client, dans un composant Providers placé dans le layout racine.
// components/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
Les changements importants de TanStack Query v5
La v5 a introduit plusieurs ruptures par rapport à la v4 qu'il faut connaître avant de migrer ou de démarrer un projet.
La signature unifiée des hooks est la plus visible : useQuery n'accepte plus que la forme objet (useQuery({ queryKey, queryFn })). La forme avec arguments positionnels est supprimée. Ce changement améliore la lisibilité et facilite l'autocomplétion TypeScript.
Le renommage de cacheTime en gcTime (garbage collection time) correspond mieux à la réalité : il s'agit du délai avant que les données inactives soient supprimées du cache, pas d'une durée de cache au sens classique.
L'utilitaire queryOptions() est l'ajout le plus impactant pour les projets maintenus dans le temps. Il permet de définir des configurations de requête réutilisables et type-safe :
import { queryOptions } from '@tanstack/react-query'
export const productOptions = (id: string) =>
queryOptions({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
staleTime: 5 * 60 * 1000,
})
// Utilisable à la fois dans prefetchQuery (serveur) et useQuery (client)
await queryClient.prefetchQuery(productOptions('42'))
const { data } = useQuery(productOptions('42'))
Cette approche garantit que queryKey et queryFn restent synchronisés entre le code serveur et le code client, supprimant une classe entière de bugs silencieux difficiles à diagnostiquer.
Invalidation et mutations cohérentes
Le préchargement n'est utile que si le cache reste cohérent lors des mutations. TanStack Query propose useMutation couplé à invalidateQueries pour gérer cela proprement.
const mutation = useMutation({
mutationFn: updateProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
La hiérarchie des clés est un point fort : invalider ['products'] invalide aussi ['products', '42'] et ['products', 'list']. C'est le modèle préfixe, du général au particulier.
Streaming de requêtes en attente
Depuis la version 5.40.0, TanStack Query supporte la déshydratation de requêtes en attente. On n'a plus besoin d'attendre la résolution d'un prefetchQuery pour commencer le rendu : la requête démarre côté serveur et ses résultats sont streamés vers le client via les capacités de React 18.
// On ne met pas await : le prefetch démarre sans bloquer le rendu
queryClient.prefetchQuery({
queryKey: ['recommendations'],
queryFn: fetchRecommendations,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<Skeleton />}>
<Recommendations />
</Suspense>
</HydrationBoundary>
)
Ce pattern est détaillé dans le guide Advanced Server Rendering de TanStack. Il réduit le Time to First Byte (TTFB) sans sacrifier la complétude des données pour les composants critiques.
Ce qu'on en retient
Associer TanStack Query v5 au App Router de Next.js demande de comprendre le flux de données : préchargement sur le serveur, sérialisation avec dehydrate, injection via HydrationBoundary, consommation avec useQuery. Le résultat est une interface qui se rend instantanément, sans spinner, tout en conservant toutes les capacités de revalidation et de mutation côté client.
Pour tout projet Next.js qui dépasse la simple lecture de données statiques, cette combinaison est aujourd'hui le choix par défaut chez Kreio. La courbe d'apprentissage est courte, et les gains en expérience utilisateur sont immédiats.