Lorsque vous développez une application Next.js, vous pouvez rencontrer deux erreurs courantes : "Window is Not Defined" et "Hydration Mismatch".
Dans cet article, je vais vous montrer comment résoudre ces erreurs définitivement.
Le JS côté client dans Next.js est un problème difficile
Le code JavaScript qui ne fonctionne que dans le navigateur est une source de confusion et de bugs dans les applications React, même pour les développeurs expérimentés. C'est très suprenant, car React est justement une librairie frontend. C'est à cause du rendu serveur, qui complique beaucoup ce problème.
Malgré les nombreux articles écrits sur ce sujet, je n'ai pas pu trouver une ressource vraiment exhaustive. Alors j'ai fait de mon mieux pour en créer une !
Si vous avez cliqué sur cet article, c'est probablement parce que vous avez rencontré l'une des deux erreurs les plus redoutées de l'histoire du JavaScript moderne :
- "window is not defined"
- "hydration failed", hydration mismatch", "text content does not match server-rendered HTML", ou tout autre message douteux concernant un manque d'hydratation ou un excès d'hydratation ☔
Voici deux exemples de code qui génèrent ces erreurs dans Next.js :
// BOUM !
// Pendant le rendu serveur,
// ce code va générer l'erreur "window n'est pas défini"
return <p>Le titre de cette page est : {window.title}</p>
// BOUM !
// Pendant le premier rendu client,
// ce code va générer m'erreur "hydration mismatch"
if (typeof window === 'undefined') {
return <p>Le titre de cette page est : {window.title}</p>
}
Je vais vous montrer comment vous en débarrasser définitivement. À la fin de cet article, vous aurez 6 nouvelles techniques dans votre boîte à outils pour gérer le code côté client dans Next.js.
Pour ceux qui veulent aller plus loin, le concept de "Browser Component" explique mieux les causes de ces erreurs. Il est détaillé dans la version longue de cet article (en anglais) et dans mon cours complet sur le code client.
Six techniques pour éliminer les erreurs "window is not defined" et "hydration mismatch"
J'ai classé les solutions en fonction du scénario qu'elles résolvent, afin de faciliter le déboguage.
1) Mon HTML est invalide.
Tout d'abord, débarrassons-nous d'un problème très courant qui génère des erreurs d'hydratation : le HTML invalide !
Parfois, l'erreur d'hydratation n'est pas due à votre code React du tout, mais à une simple erreur dans votre code HTML.
Par exemple, vous pouvez avoir deux balises "<html>", répétées dans différents layouts.
Next.js 15 détecte ces erreurs plus clairement que les versions précédentes.
2) Mon composant a besoin d'une bibliothèque spécifique au navigateur comme Leaflet ou jQuery.
Ces bibliothèques plantent lorsqu'elles sont importées côté serveur, car elles s'attendent à ce que l'objet window
soit disponible globalement.
// BOOM !
// Cela déclenchera l'erreur "window is not defined"
// pendant le rendu côté serveur
import 'react-leaflet'
Un composant qui les importe ne fonctionne que dans le navigateur. J'appelle cela un "Browser Component". C'est un composant dangereux, il est donc nécessaire de créer un second composant qui le rend inoffensif.
La solution consiste à utiliser le lazy loading avec next/dynamic
et ssr: false
pour importer votre composant.
'use client'
import dynamic from 'next/dynamic'
// Ce composant est maintenant sûr à utiliser dans Next
// Il enveloppe "BrowserComponent"
// pour le transformer en Client Component
export const ClientComponent = dynamic(
() =>
import('./BrowserComponent')
// cette partie est nécessaire
/// si vous utilisez un export nommé
// vous pouvez la remplacer par ".default" sinon
.then((mod) => mod.BrowserComponent),
{
// Cela empêche le rendu côté serveur de BrowserComponent
ssr: false,
}
)
Maintenant, vous pouvez utiliser ClientComponent
en toute sécurité.
3) Mon composant utilise l'objet window ou tout autre code spécifique au navigateur pour afficher quelque chose. Je peux modifier son code.
Vous pouvez utiliser le hook useMounted
que je fournis ci-dessous pour réécrire votre composant.
export const useMounted = () => {
const [mounted, setMounted] = useState<boolean>()
// les effets ne fonctionnent que côté client
// donc nous pouvons détecter quand le composant est hydraté/monté
// @see https://react.dev/reference/react/useEffect
useEffect(() => {
setMounted(true)
}, [])
return mounted
}
Vous pouvez trouver un morceau de code similaire dans de nombreux endroits et sous différents noms comme “useHydrated”, “useClient”… La documentation Next.js appelle ce hook “useClient” mais je trouve “useMounted” plus explicite.
Lorsque “mounted” est vrai, cela signifie que vous pouvez utiliser l'objet window
pour rendre quelque chose.
// Ceci est un Client Component :
// il peut être rendu en toute sécurité sur le serveur
// (il n'affichera rien là-bas mais c'est ok)
// Ensuite, il affichera le titre de la fenêtre côté client
export function ClientComponent({ children }) {
const mounted = useMounted()
if (!mounted) return null
return <p>{window.title}</p>
}
Vous pouvez découvrir une autre implémentation de "useMounted" ici.
4) Mon composant utilise l'objet window ou tout autre code spécifique au navigateur pour rendre quelque chose. Malheureusement, je ne peux pas modifier son code.
Idéalement, le composant devrait être réécrit (voir paragraphe ci-dessus), mais comme vous ne pouvez pas contrôler le code, vous ne pourrez peut-être pas le faire.
La première solution consiste à utiliser useMounted
dans le parent qui rend le composant fautif.
'use client'
export function ClientComponent() {
const mounted = useMounted()
if (mounted) return <BrowserComponent />
}
Cela est très similaire à la solution précédente, où nous avons utilisé le hook useMounted
pour vérifier s'il était sécuritaire d'utiliser l'objet window
pendant le rendu.
Cela signifie que vous devez créer un nouveau composant parent, et que ce composant doit être un composant client pour pouvoir utiliser un hook. Cela peut être fastidieux.
La deuxième solution consiste à créer un composant NoSsr
.
Vous devez simplement envelopper le composant de navigateur problématique dans un NoSsr
,
et il ne sera pas prérendu sur le serveur.
// Un composant NoSsr réutilisable
'use client'
import React from 'react'
import { useMounted } from '../hooks/useMounted'
export function NoSsr({ children }) {
// Voir section précédente
// pour "useMounted"
const mounted = useMounted()
if (!mounted) return null
return <>{children}</>
}
Utilisation :
'use client'
export function ClientComponent() {
return (
<NoSsr>
<BrowserComponent />
</NoSsr>
)
}
Un autre composant encore plus élaboré permet de contrôler le rendu de composants côté client: SkipRenderOnClient.
5) Mon composant a besoin d'une bibliothèque qui peut être importée, mais pas utilisée côté serveur, comme D3 pour la dataviz.
Plus largement, c'est le cas où vous voulez faire des modifications impératives du DOM.
Vous pouvez en faire un Client Component grâce au hook useEffect
. En effet, les effets ne s'exécutent que lorsque le composant est monté, et les composants sont montés uniquement dans le navigateur.
Cela signifie que vous pouvez utiliser l'objet window
en toute sécurité dans les callbacks d'effet !
Notez que nous avons utilisé cette belle propriété du hook useEffect
pour créer notre hook useMounted
.
// Dans mon composant client
useEffect(() => {
if (divRef.current) {
divRef.innerText = 'Chargé !'
}
}, [])
return <div ref={divRef}>Chargement...</div>
Ce code affichera un loader pendant le rendu côté serveur, mais c'est tout à fait normal.
6) Mon code est exactement le même, mais se comporte légèrement différemment dans le navigateur et sur le serveur.
Cela se produit lors de l'utilisation de dates ou des fonctionnalités de localisation de JavaScript. Vous avez le même code côté client et côté serveur mais il génère un résultat différent.
Ce cas est très délicat, je vais donc partager directement la solution.
Vous avez besoin d'un composant qui encapsule le code dans un Suspense
et qui se re-rend après avoir été monté :
'use client'
import React, { Suspense } from 'react'
import { useMounted } from '../hooks/useMounted'
export function SafeHydrate({ children }: { children: React.ReactNode }) {
const isMounted = useMounted()
return <Suspense key={isMounted ? 'client' : 'server'}>{children}</Suspense>
}
Je dois cette astuce à un article de François Best, vous trouverez le lien dans la section "Ressources" ci-dessous.
Il existe d'autres solutions à ce problème, mais je ne les ai pas trouvées satisfaisantes :
- La propriété "supressHydrationWarning" de React peut supprimer l'erreur d'hydratation, mais la dernière fois que j'ai vérifié, elle continuait à afficher la valeur rendue par le serveur et ne mettait pas à jour la valeur côté client. Je ne sais pas si c'est un bug ou si c'est intentionnel.
- J'ai découvert l'API expérimentale "unstable_postpone" de React (merci à Andrew Ingram), qui semble désactiver complètement le rendu côté serveur.
SafeHydrate
, en revanche, affichera correctement la valeur rendue par le serveur, puis la valeur rendue côté client.
Vous y êtes !
Merci beaucoup d'avoir lu cet article !
Avec ces 6 solutions, vous êtes solidement équipé pour vous débarrasser des redoutables erreurs "window is not defined" et "hydration mismatch" !
Si vous rencontrez un problème spécifique avec le code côté client que vous ne pouvez toujours pas résoudre, laissez un commentaire ou contactez-moi sur X/Twitter (@ericbureltech) : je ferai de mon mieux pour trouver une solution à votre problème.
Envie d'aller plus encore loin ?
Cet article est un résumé de ma récente formation vidéo : "Manage client-only code in Next.js with React Browser Components".
Si vous avez aimé cet article et que vous souhaitez vous améliorer dans la gestion du code JavaScript côté client dans Next.js, cliquez ici pour découvrir le cours en ligne.
À bientôt !
Ressources
- Mon cours sur la gestion du code côté client dans Next.js : https://discover.nextpatterns.dev/client-only
- Mes autres cours Next.js : https://nextpatterns.dev/
- Documentation de Next.js sur les problèmes d'hydratation : https://nextjs.org/docs/messages/react-hydration-error
- Lazy loading dans Next.js avec next/dynamic : https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading
- Documentation de React sur useEffect : https://react.dev/reference/react/useEffect
- Question StackOverflow sur le chargement du code côté client avec next/dynamic : https://stackoverflow.com/questions/68178127/next-js-with-react-leaflet-window-is-not-defined-when-refreshing-page
- Question StackOverflow sur l'utilisation de l'objet window : https://stackoverflow.com/questions/55151041/window-is-not-defined-in-next-js-react-app/77556387#77556387
- Article de François Best sur le scénario où le même code se comporte différemment côté serveur et côté client : https://francoisbest.com/posts/2023/displaying-local-times-in-nextjs
- Ma question à Dan Abramov pour savoir si le prérendu du composant client est une fonctionnalité React ou Next (c'est une fonctionnalité React) : https://twitter.com/ericbureltech/status/1707729362777231814
- Une variation du composant NoSsr utilisant un store externe synchronisé vide, par Dominik (TkDodo). L'avantage potentiel par rapport à "useEffect" est qu'il pourrait "différer le rendu lors d'une transition côté client" : https://twitter.com/TkDodo/status/1741068994981826947
- SkipRenderOnClient : une astuce pour éviter le problème de double rendu pour les applications responsives que j'ai découvert sur Twitter grâce à Sebastien Lorber (je n'ai pas encore eu l'occasion de l'essayer, faites-moi part de vos retours si vous l'avez fait !) : https://twitter.com/sebastienlorber/status/1742528219318738955 Il semble être utilisé par le package "@artsy/fresnel" : https://artsy.github.io/blog/2019/05/24/server-rendering-responsively/