Tous les articles
·Ingénierie·8 min

TanStack Query v5 avec Next.js App Router : préchargement et hydratation

Comment combiner TanStack Query v5 et le App Router de Next.js pour éliminer les états de chargement et livrer des pages hydratées côté client.

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.

Schéma du flux de données entre serveur et client avec TanStack Query et Next.js
Le flux préchargement serveur vers hydratation client élimine les spinners côté 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ètreValeur par défautRôle
staleTime0 msDurée pendant laquelle les données sont considérées fraîches
gcTime5 minutesDuré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 t0t_0, elle est considérée fraîche tant que :

isFresh(t)=t<t0+staleTime\text{isFresh}(t) = t < t_0 + staleTime

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.

Sources

  1. Server Rendering & Hydration - TanStack Query v5
  2. Advanced Server Rendering - TanStack Query v5
  3. Query Invalidation - TanStack Query v5