Pourquoi ce concept existe
Quand Next.js sert une page SSR (Server-Side Rendered), il envoie au navigateur deux choses :
- Du HTML déjà rendu (pour que l'user voie quelque chose tout de suite, même avant que JS arrive)
- Du JavaScript React qui, une fois chargé, doit "hydrater" ce HTML — comprendre : attacher les event handlers, retrouver le
useState, etc., sans tout re-rendre.
L'hydratation, c'est cette phase où React "adopte" le HTML reçu et le rend interactif.
La règle inviolable
Le HTML produit côté serveur doit être strictement identique à celui que React produirait côté client. Sinon, React détecte une mismatch, abandonne l'arbre, re-rend tout depuis zéro, et logue une erreur rouge.
Les causes typiques de mismatch
Date.now()/Math.random()— Serveur et client génèrent à des moments différentsnew Date().toLocaleString()sans locale fixe — Node = en-US, navigateur = locale usertypeof window !== "undefined"ternaire — Sur le serveurwindowestundefined, sur le client non → diff- Lecture de
localStorageau premier render — n'existe pas sur le serveur - Modification du DOM par un script tiers avant que React démarre (ex : extension navigateur, script de thème)
- Balise HTML mal nestée (ex :
<p>dans un<p>) — le browser réécrit le DOM, React voit la diff
Comment ça se manifeste
Console :
"Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client."
Conséquence pratique :
- L'arbre est re-rendu en CSR (Client-Side) → flash visuel possible
- Les
useEffectne s'exécutent qu'après le re-rendu réussi - Si l'erreur est fatale (rare), l'arbre client n'attache jamais → ton app est figée
Les bonnes parades
Pattern 1 — Gate avec useState + useEffect : rendre la valeur cliente uniquement après hydration.
"use client";
const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);
return <span>{hydrated ? new Date().toLocaleString() : "…"}</span>;Bug 1er paint mais SSR et hydration matchent.
Pattern 2 — suppressHydrationWarning : utiliser uniquement quand la diff est volontaire et limitée à 1 nœud.
<html lang="fr" suppressHydrationWarning>C'est ce qu'on fait dans app/src/app/layout.tsx parce qu'un script de thème ajoute dark à <html> avant React.
Pattern 3 — Format déterministe : remplacer toLocaleString() par un format identique partout.
const fmt = (n: number) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
// "4925" → "4 925" — pareil en Node et navigateurAnecdote wari
On a eu ce bug le 23 mai 2026 sur dev-tour (BUG-002) : stats.totalNodes.toLocaleString() produisait "4,925" côté Node (en-US) et "4 925" côté navigateur (fr-FR). React détectait la diff, abandonnait l'hydration, et la home restait figée sur "Chargement…" jusqu'au refresh. Fix : formatter déterministe + sortir le <script> de thème de l'arbre React.