Frontend·3 min de lecture

React : l'hydratation

Pourquoi ce concept existe

Quand Next.js sert une page SSR (Server-Side Rendered), il envoie au navigateur deux choses :

  1. Du HTML déjà rendu (pour que l'user voie quelque chose tout de suite, même avant que JS arrive)
  2. 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

  1. Date.now() / Math.random() — Serveur et client génèrent à des moments différents
  2. new Date().toLocaleString() sans locale fixe — Node = en-US, navigateur = locale user
  3. typeof window !== "undefined" ternaire — Sur le serveur window est undefined, sur le client non → diff
  4. Lecture de localStorage au premier render — n'existe pas sur le serveur
  5. Modification du DOM par un script tiers avant que React démarre (ex : extension navigateur, script de thème)
  6. 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 useEffect ne 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 navigateur

Anecdote 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.

Dans ton code wari

  • app/src/app/layout.tsxRoot layout — `suppressHydrationWarning` sur <html> car le thème modifie className avant React