Next.js hydration error signifie que le HTML produit par le serveur ne correspond pas au premier rendu calculé dans le navigateur. Pour corriger l’erreur de réhydratation, rendez le rendu initial déterministe, déplacez les accès à window, localStorage et à la date dans useEffect, puis utilisez suppressHydrationWarning uniquement pour une différence volontaire et localisée. Commencez par lire le premier composant indiqué dans la console, pas par désactiver le rendu serveur.
Cette erreur apparaît souvent après une migration vers l’App Router. Notre guide de l’App Router Next.js détaille les frontières entre Server Components et Client Components, l’ajout d’un thème sombre, l’affichage d’une date locale ou l’utilisation d’une librairie qui suppose que le navigateur existe. Elle n’est pas un simple avertissement esthétique : elle peut remplacer le HTML livré par le serveur, provoquer un flash d’interface, casser des interactions et dégrader le SEO d’une page rendue côté serveur.
Next.js hydration error : comprendre précisément le décalage
L’hydratation est l’étape durant laquelle React attache ses événements et son état au HTML déjà envoyé par Next.js. Le serveur rend par exemple un bouton avec le texte « Se connecter ». Le navigateur exécute ensuite le même composant. Si celui ci obtient « Déconnexion » parce qu’il lit un cookie différemment, React détecte deux arbres différents. Le HTML existant ne peut alors pas être réutilisé de manière fiable.
Le rendu initial doit donc être pur et reproductible. Une même entrée doit produire la même sortie côté serveur et au premier passage côté client. Les valeurs aléatoires, l’horloge, le fuseau local, la largeur de fenêtre, le stockage du navigateur et certaines extensions sont les causes les plus fréquentes. En pratique, un message comme Hydration failed because the initial UI does not match désigne un problème de contrat entre ces deux rendus.
Error: Hydration failed because the initial UI does not match
Warning: Text content did not match. Server: "14:00" Client: "15:00"
See more info here: https://nextjs.org/docs/messages/react-hydration-error
Le problème est différent d’une erreur JavaScript classique. Une exception dans un composant empêche le rendu. Une erreur d’hydratation laisse souvent un écran visible, mais React doit reconstruire une partie de l’arbre. Il faut donc corriger la cause, puis vérifier le rendu en production, car le mode de développement ajoute parfois des contrôles supplémentaires.
Cause numéro un : window, document et localStorage pendant le rendu
window, document, navigator et localStorage n’existent pas sur le serveur. Le test typeof window !== 'undefined' évite une exception, mais il ne garantit pas une hydratation correcte. Si le serveur rend « invité » et le navigateur rend immédiatement « membre », le contenu diffère tout de même au premier passage.
La solution robuste consiste à rendre une valeur neutre des deux côtés, puis à lire l’API navigateur dans un effet après l’hydratation. Le composant peut afficher un état de chargement très court, ou recevoir l’information depuis le serveur lorsqu’elle est disponible dans une session sécurisée.
'use client'
import { useEffect, useState } from 'react'
export function ThemeLabel() {
const [theme, setTheme] = useState<'light' | 'dark' | null>(null)
useEffect(() => {
const saved = window.localStorage.getItem('theme')
setTheme(saved === 'dark' ? 'dark' : 'light')
}, [])
return <span>{theme === null ? 'Chargement du thème' : theme}</span>
}
Évitez aussi de calculer une classe CSS différente avec window.innerWidth. Le CSS responsive est préférable. Pour un menu, rendez le même bouton initial, puis activez l’état ouvert après un clic. Pour une API qui dépend du navigateur, déclenchez la requête dans useEffect ou utilisez une solution de données compatible avec le rendu serveur.
Dates, fuseaux horaires et valeurs aléatoires
Une date formatée avec toLocaleString() peut changer entre le serveur situé en UTC et le navigateur situé en Europe/Brussels. Même une date fixe devient différente si le serveur et le client ne partagent pas le même fuseau. Date.now(), Math.random() et les identifiants générés à l’exécution créent le même risque.
Pour un contenu important, formatez la date sur le serveur avec un fuseau explicite et transmettez une chaîne déjà calculée. Pour un temps relatif ou une horloge vivante, affichez une valeur stable au premier rendu et mettez la valeur à jour après le montage. Dans une liste, utilisez une clé stable issue de la base de données, jamais un nombre aléatoire ou l’index quand l’ordre peut changer.
import { useEffect, useState } from 'react'
export function RelativeDate({ iso }: { iso: string }) {
const [label, setLabel] = useState('Date disponible')
useEffect(() => {
const date = new Date(iso)
setLabel(new Intl.RelativeTimeFormat('fr', { numeric: 'auto' }).format(-1, 'jour'))
}, [iso])
return <time dateTime={iso}>{label}</time>
}
Si l’heure exacte est indispensable, transmettez une date ISO et affichez d’abord cette représentation identique. Un second rendu peut ensuite présenter le format local. Cette approche évite de masquer une divergence réelle sous un avertissement silencieux.
Client Components et App Router : placer la frontière au bon endroit
Dans l’App Router, un composant est un Server Component par défaut. La directive 'use client' crée une frontière qui autorise les hooks et les événements, mais le composant peut quand même être pré rendu sur le serveur. Ajouter cette directive ne supprime donc pas l’hydratation. Cela augmente seulement la surface de code exécutée dans le navigateur.
Gardez les données et les décisions accessibles au serveur dans le parent. Passez des propriétés sérialisables au composant interactif. Ne transmettez pas une fonction, une instance de classe ou une valeur calculée avec une API navigateur. Pour une librairie qui ne fonctionne qu’au client, notre article sur Next.js, Tailwind et TypeScript pour le SEO technique fournit un exemple de stack cohérente, puis isolez le composant et chargez le module dynamiquement avec le SSR désactivé, uniquement si l’interface n’a pas besoin d’être présente dans le HTML initial.
import dynamic from 'next/dynamic'
const ClientOnlyChart = dynamic(() => import('./client-only-chart'), {
ssr: false,
loading: () => <p>Chargement du graphique</p>,
})
export default function Dashboard() {
return <ClientOnlyChart />
}
ssr: false est un filet de sécurité pour un composant réellement dépendant du DOM, comme certains éditeurs ou graphiques. Ne l’appliquez pas à toute la page : vous perdriez le HTML utile, le référencement et une partie des bénéfices de l’App Router. D’abord, cherchez si la librairie propose un mode serveur ou une initialisation différée.
HTML invalide, composants tiers et extensions du navigateur
Un HTML mal imbriqué peut déclencher une erreur même si les données sont identiques. Un <p> contenant un autre paragraphe, un bouton placé dans un lien ou des balises fermées dans le mauvais ordre sont des coupables classiques. Le navigateur corrige automatiquement ce HTML avant que React ne tente de l’hydrater, ce qui crée deux arbres différents.
Validez la structure des composants qui entourent le message. Les bibliothèques de modales, d’icônes et de tableaux peuvent aussi produire des identifiants différents ou injecter un nœud avant l’hydratation. Mettez à jour la dépendance, consultez ses notes de compatibilité App Router et reproduisez le problème dans une page minimale.
// À éviter
<p>
Résultat : <p>42</p>
</p>
// Préférer
<div>
<p>Résultat : <span>42</span></p>
</div>
Une extension de navigateur qui modifie le DOM, comme certains outils de traduction, de correction ou de thème sombre, peut également provoquer un message uniquement sur votre poste. Testez une fenêtre privée sans extension, un autre navigateur et une build de production. Si l’erreur disparaît, ce n’est pas une raison pour ignorer une erreur observée par vos utilisateurs, mais il faut distinguer la cause applicative de la modification locale.
Diagnostic méthodique en développement et en production
Commencez par supprimer le cache de compilation avec rm -rf .next, relancez le serveur et reproduisez sur une URL précise. Lisez la première trace de composant mentionnée dans la console. Ajoutez temporairement des marqueurs textuels côté serveur et côté client, plutôt que des logs qui se mélangent dans deux terminaux. Ensuite, testez une build production, car le rendu statique, le streaming et les variables d’environnement peuvent changer le chemin d’exécution.
Une bonne vérification consiste à comparer le HTML reçu et le DOM après exécution. Utilisez un navigateur vierge, désactivez les extensions, contrôlez le fuseau horaire et imposez les mêmes variables d’environnement. Vérifiez également les données qui changent entre la génération et la requête : session, cookies, géolocalisation, expérimentation et réponse API non déterministe.
rm -rf .next
npm run build
NODE_ENV=production npm run start
# Chercher les accès navigateur dans les composants rendus
rg "window|document|localStorage|Date.now|Math.random|toLocale" app components
Ajoutez un test de fumée qui ouvre les routes critiques avec JavaScript activé et inspecte les erreurs de console. Le test doit couvrir une session anonyme et une session connectée. Pour un site fortement statique, comparez aussi le HTML de sortie dans plusieurs régions, car le cache ou une fonction edge peut modifier les données initiales.
Quand utiliser suppressHydrationWarning et quand le refuser
suppressHydrationWarning ne corrige pas le rendu. Il demande à React de ne pas signaler une différence sur un élément précis, principalement pour une valeur volontaire comme une date générée par un outil externe. Il ne doit pas entourer un grand panneau ni servir à cacher un problème de session, de thème ou de données.
export function ServerTimestamp({ value }: { value: string }) {
return (
<time dateTime={value} suppressHydrationWarning>
{value}
</time>
)
}
Avant de l’utiliser, demandez si la différence est attendue, localisée et sans conséquence sur l’accessibilité. Si la réponse est non, rendez les deux sorties identiques ou déplacez le calcul après l’hydratation. Une alerte supprimée peut cacher une fuite de données, une interface inaccessible ou une page qui n’est pas indexable comme prévu.
Si votre projet combine un CMS, l’article WordPress headless avec Next.js aide à séparer les problèmes de données serveur des problèmes d’hydratation.
FAQ sur Next.js hydration error
Pourquoi Next.js affiche t il une erreur d’hydratation ?
Next.js affiche une erreur d’hydratation lorsque le HTML rendu par le serveur diffère du premier rendu exécuté dans le navigateur. Les causes fréquentes sont window, les dates, les valeurs aléatoires, un HTML invalide et des données qui changent entre les deux étapes.
Comment corriger une erreur d’hydratation liée à localStorage ?
Rendez une valeur neutre identique côté serveur et client, puis lisez localStorage dans un useEffect. Si le contenu doit être connu dès le premier rendu, transmettez plutôt la préférence depuis un cookie lu côté serveur.
Faut il utiliser suppressHydrationWarning ?
Utilisez suppressHydrationWarning seulement pour une différence volontaire, limitée à un élément comme une date. Pour un thème, une session ou une donnée métier, corrigez la cause afin de conserver un HTML fiable et accessible.
Pourquoi l’erreur existe seulement en production ?
La production peut activer le cache, le rendu statique, le streaming ou un chemin edge différent du serveur de développement. Reproduisez avec next build et next start, puis vérifiez les cookies, les variables d’environnement et la réponse API.
Commentaires (0)
Laisser un commentaire
Les commentaires sont modérés. Questions WordPress, cybersécurité ou dev web bienvenues.