Next.js window is not defined signifie que votre code utilise l’objet window pendant le rendu serveur, alors que cet objet n’existe que dans le navigateur. La correction la plus fiable consiste à déplacer l’accès à window, document ou localStorage dans un useEffect d’un Client Component. Pour une bibliothèque qui dépend entièrement du navigateur, chargez le composant avec dynamic et ssr: false. Évitez de masquer le problème avec un simple test qui produit un HTML différent entre serveur et client.

Cette erreur apparaît lors de next dev, de next build ou uniquement en production. Elle touche l’App Router comme le Pages Router, les composants React et de nombreuses bibliothèques d’interface. Voici une méthode de diagnostic qui distingue le rendu serveur, l’hydratation, le chargement dynamique et le stockage local.

Pourquoi Next.js window is not defined apparaît

Next.js exécute une partie de votre application dans un environnement serveur afin de produire du HTML rapidement, d’améliorer le référencement et de réduire le travail initial du navigateur. Dans cet environnement, le code dispose de Node.js, mais pas des objets Web fournis par un navigateur. window, document, localStorage, sessionStorage, navigator et certaines API de positionnement ne sont donc pas disponibles.

Le piège vient du fait qu’un composant peut fonctionner après un clic dans votre navigateur, tout en échouant avant même que la page soit envoyée. Une lecture exécutée au niveau du module est particulièrement dangereuse, car elle se déclenche dès l’import, avant le premier rendu. Une lecture dans le corps d’un composant peut aussi casser le rendu serveur, même si le fichier contient la directive use client. Cette directive autorise les API React interactives, mais elle ne transforme pas le serveur en navigateur.

// Mauvais : cette lecture arrive pendant le rendu serveur
'use client'

const largeur = window.innerWidth

export default function Page() {
  return <p>Largeur : {largeur}</p>
}

Pour replacer l’erreur dans le cycle complet, consultez aussi notre guide sur Next.js hydration error. Les deux problèmes sont proches, mais une variable navigateur absente provoque d’abord une exception, tandis qu’une valeur différente peut ensuite provoquer une divergence d’hydratation.

La correction recommandée avec useEffect

Utilisez useEffect pour les lectures qui n’ont de sens qu’après le montage dans le navigateur. React n’exécute pas cet effet pendant le rendu serveur. Vous rendez d’abord un état neutre identique des deux côtés, puis vous remplacez cet état quand le client est prêt. Cette approche évite l’exception et limite le risque d’hydratation incohérente.

'use client'

import { useEffect, useState } from 'react'

export default function ViewportWidth() {
  const [width, setWidth] = useState<number | null>(null)

  useEffect(() => {
    const update = () => setWidth(window.innerWidth)
    update()
    window.addEventListener('resize', update)
    return () => window.removeEventListener('resize', update)
  }, [])

  return <p>{width === null ? 'Mesure en cours' : `Largeur : ${width}px`}</p>
}

Le même principe vaut pour les écouteurs d’événements, les médias, le presse-papiers et les API de géolocalisation. Ajoutez toujours une fonction de nettoyage pour les abonnements. Une erreur fréquente consiste à initialiser la valeur avec window.innerWidth dans useState. Cela déclenche encore l’erreur, car l’initialiseur est évalué pendant le rendu. Commencez par null, zéro ou une valeur métier neutre.

Corriger localStorage et sessionStorage

localStorage n’est pas un objet global disponible dans Node.js. Il faut donc lire et écrire le stockage depuis un effet, puis prévoir le cas où aucune valeur n’existe. Cette séparation améliore également la robustesse lorsque l’utilisateur bloque le stockage, navigue en mode privé ou atteint une restriction de quota.

'use client'

import { useEffect, useState } from 'react'

export function ThemePreference() {
  const [theme, setTheme] = useState('system')

  useEffect(() => {
    const saved = window.localStorage.getItem('theme')
    if (saved === 'light' || saved === 'dark') setTheme(saved)
  }, [])

  useEffect(() => {
    try {
      window.localStorage.setItem('theme', theme)
    } catch {
      // Le stockage peut être indisponible ou saturé
    }
  }, [theme])

  return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
    Thème : {theme}
  </button>
}

Ne construisez pas l’état initial à partir de localStorage dans le rendu. Même un garde comme typeof window !== 'undefined' peut produire une valeur serveur différente de la valeur client. Le serveur affiche alors une interface, puis React en découvre une autre au moment de l’hydratation. Pour une préférence visuelle, utilisez éventuellement un cookie lu côté serveur, car le cookie peut participer au rendu initial.

Utiliser typeof window sans créer une divergence

Le test typeof window !== 'undefined' est utile dans du code partagé qui doit seulement savoir dans quel environnement il se trouve. Il ne faut pas l’utiliser pour afficher immédiatement une valeur différente dans le JSX. Le test est sûr pour éviter une exception dans une fonction d’adaptation, mais il ne règle pas automatiquement la synchronisation serveur et client.

export function isBrowser() {
  return typeof window !== 'undefined'
}

export function readPageUrl() {
  if (!isBrowser()) return null
  return window.location.href
}

Dans un composant, préférez souvent un état et un effet. Si le rendu doit absolument dépendre d’une valeur connue uniquement après le chargement, affichez un substitut stable pendant le premier rendu. Pour un composant non essentiel au référencement, vous pouvez aussi le rendre uniquement côté client. L’important est de décider explicitement quelle version doit être visible avant l’exécution de JavaScript.

Charger une bibliothèque navigateur avec dynamic

Les cartes, éditeurs, graphiques et certains widgets accèdent à window dès leur import. Dans ce cas, déplacer seulement votre appel dans useEffect ne suffit pas, car l’exception survient avant l’effet. Isolez la bibliothèque dans un composant et importez-le dynamiquement avec ssr: false.

import dynamic from 'next/dynamic'

const MapClient = dynamic(() => import('./MapClient'), {
  ssr: false,
  loading: () => <p>Chargement de la carte</p>
})

export default function Page() {
  return <main>
    <h1>Nos bureaux</h1>
    <MapClient />
  </main>
}

Le composant importé doit rester clairement client. Ajoutez 'use client' dans son fichier si vous utilisez des hooks ou des événements. Cette solution évite l’exécution serveur, mais elle a un coût : le HTML de la bibliothèque n’est pas disponible pour le référencement et l’interface attend le téléchargement du JavaScript. Gardez donc un titre, un texte ou une image de remplacement rendus par le serveur.

Avec l’App Router, ssr: false doit être appliqué dans un Client Component. Si Next.js refuse l’option dans un Server Component, créez un petit composant relais avec 'use client', puis placez l’import dynamique dedans. Cette contrainte est normale : le chargement client est une décision d’exécution côté navigateur.

Corriger document is not defined et navigator is not defined

document is not defined suit exactement la même logique. Les appels à document.querySelector, document.title ou document.body doivent être déclenchés après le montage, sauf si vous les remplacez par une API React. Pour manipuler une classe, préférez l’état et une classe conditionnelle. Utilisez l’API DOM uniquement quand une intégration externe l’exige.

'use client'

import { useEffect } from 'react'

export function FocusSearch() {
  useEffect(() => {
    const input = document.querySelector<HTMLInputElement>('#search')
    input?.focus()
  }, [])

  return <input id="search" name="q" />
}

Pour navigator, attendez aussi l’effet pour lire la langue, le type de connexion ou les capacités du navigateur. Ne considérez pas ces données comme une vérité de sécurité. Elles peuvent être absentes, falsifiées ou modifiées après le premier rendu. Si elles influencent le contenu, prévoyez un état de chargement et une valeur de repli.

Éviter les erreurs dans les modules et les bibliothèques

Une cause moins visible est l’import d’un module qui exécute du code navigateur au niveau supérieur. Le fichier de votre composant peut sembler correct, mais une dépendance importée peut appeler window dès son initialisation. Identifiez la trace complète, puis recherchez le premier fichier de votre code qui déclenche l’import. Un import dynamique côté client est souvent la correction la plus propre.

// À éviter dans un module évalué côté serveur
const saved = window.localStorage.getItem('token')

// Préférer une fonction appelée depuis useEffect
export function loadToken() {
  if (typeof window === 'undefined') return null
  return window.localStorage.getItem('token')
}

Ne mettez jamais un jeton sensible dans localStorage uniquement pour faire disparaître l’erreur. Les scripts injectés dans la page pourraient le lire. Pour une session web, évaluez des cookies sécurisés et HttpOnly avec votre architecture d’authentification. Le diagnostic SSR ne doit pas conduire à une faiblesse de sécurité.

Tester le correctif avec next build

Un serveur de développement ne suffit pas toujours. Lancez une compilation de production, car le pré rendu, les imports et les routes peuvent être traités différemment. Vérifiez également une navigation directe vers la route, un rafraîchissement complet et une navigation depuis une autre page.

rm -rf .next
npm run lint
npm run build
npm run start

# Rechercher les accès directs suspects
grep -R "window.|document.|localStorage|sessionStorage" -n app components lib

Pour un test automatisé, rendez d’abord le composant dans un environnement serveur ou utilisez une suite qui simule l’absence de navigateur. Ajoutez ensuite un test de comportement côté client. Le but n’est pas de tester que window existe, mais de vérifier que l’interface possède un état initial valide et qu’elle se met à jour après le montage.

Si l’erreur apparaît seulement après déploiement, comparez la version de Node.js, le mode de sortie, les variables d’environnement et la dépendance concernée. Une version de bibliothèque peut introduire un accès navigateur au niveau du module. Verrouillez les dépendances et inspectez le journal de build avant de modifier plusieurs fichiers.

Checklist de dépannage rapide

Commencez par lire la première ligne de la trace, pas uniquement le dernier fichier affiché. Repérez si l’accès se produit à l’import, pendant le rendu ou dans un effet. Ensuite choisissez la correction adaptée : useEffect pour une lecture après montage, cookie pour une valeur nécessaire au rendu serveur, dynamic pour une bibliothèque entièrement navigateur, ou garde d’environnement pour une fonction partagée.

1. Identifier l’API absente
2. Vérifier si l’accès est au niveau du module
3. Déplacer la lecture dans useEffect
4. Isoler une dépendance navigateur si nécessaire
5. Rendre un état initial identique
6. Exécuter lint, build et test ciblé
7. Tester une navigation directe en production

Pour approfondir l’architecture des routes et des composants, notre guide Next.js App Router explique la séparation entre Server Components et Client Components. Notre comparatif Next.js, SvelteKit et Qwik aide aussi à comprendre pourquoi les stratégies de rendu ne se corrigent pas de la même manière selon le framework.

Sources

FAQ : Next.js window is not defined

Comment corriger Next.js window is not defined ?

Déplacez l’accès à window dans un useEffect d’un Client Component et rendez une valeur initiale identique côté serveur et côté navigateur. Si une bibliothèque accède à window dès son import, chargez-la avec dynamic et ssr: false.

Pourquoi use client ne suffit-il pas ?

use client indique que le fichier peut utiliser les fonctionnalités interactives de React, mais le composant peut encore être pré rendu côté serveur. Les objets du navigateur ne sont disponibles qu’après l’arrivée du code dans le navigateur. Il faut donc utiliser un effet ou désactiver le rendu serveur pour le composant concerné.

Comment corriger localStorage is not defined dans Next.js ?

Lisez et écrivez localStorage dans un useEffect, avec un état initial neutre et une gestion des erreurs de quota. Si la valeur doit influencer le HTML initial, préférez un cookie lu côté serveur afin que le serveur et le client partagent la même information.

Faut-il toujours utiliser typeof window ?

Non. typeof window !== 'undefined' évite une exception dans une fonction partagée, mais il peut créer une divergence si vous l’utilisez pour produire directement deux HTML différents. Pour un composant React, un état suivi par useEffect est généralement plus clair et plus sûr.

Pourquoi l’erreur arrive-t-elle uniquement avec next build ?

La compilation de production analyse et pré rend certaines routes dans un contexte serveur qui révèle les accès navigateur masqués en développement. Lancez next build, inspectez la trace complète et recherchez les accès au niveau des modules ainsi que les dépendances qui utilisent window pendant leur import.

G
WP Admin Lab

Architecte web full-stack. WordPress, performance, data et sécurité. Notes de terrain, tests reproductibles et retours d'expérience.