12k
All articles

Résoudre l'erreur « window is not defined » dans les applications rendues côté serveur

Corrigez window is not defined dans les apps rendues côté serveur avec des hooks au montage, des gardes typeof window et des imports client-only.

OpenReplay Team
OpenReplay Team
Résoudre l'erreur « window is not defined » dans les applications rendues côté serveur

L’erreur window is not defined signifie que votre code s’est exécuté dans Node.js, où aucun objet window n’existe : les frameworks à rendu serveur exécutent d’abord vos composants sur le serveur, avant qu’un navigateur n’entre en jeu.

L’erreur survient généralement juste après l’ajout du rendu serveur à une application qui fonctionnait, ou lorsqu’on déplace un composant qui fonctionnait parfaitement côté client vers Next, Nuxt, SvelteKit, Astro ou React Router. Le composant n’a pas changé. C’est l’endroit où il s’exécute qui a changé, et la stack trace vous indique laquelle des trois solutions ci-dessous vous est nécessaire.

Points clés à retenir

  • window is not defined signifie que le code s’est exécuté dans Node.js, où window n’existe à aucun moment d’aucun cycle de vie ; il ne s’agit pas d’un problème de timing.
  • La solution par défaut consiste à déplacer l’accès dans un hook de montage (useEffect, onMounted, onMount), car les hooks de montage ne s’exécutent jamais côté serveur.
  • Une garde typeof window !== 'undefined' a sa place dans le code au niveau module et dans les utilitaires partagés ; à l’intérieur du rendu d’un composant, elle fait diverger le HTML serveur et client.
  • Le rendu exclusivement client est le dernier recours : il retire entièrement le composant du HTML généré par le serveur.
  • Le même plantage peut se produire pendant le build, car la génération statique exécute les composants dans Node pour produire du HTML.

Pourquoi l’erreur « window is not defined » survient-elle dans les applications à rendu serveur ?

Les applications à rendu serveur exécutent vos composants deux fois : une première fois dans Node.js pour produire le HTML, puis une seconde fois dans le navigateur. La portée globale de Node.js ne comprend ni window ni document : tout code qui y accède pendant la passe serveur déclenche donc une ReferenceError. L’objet n’est pas « pas encore disponible » ; dans Node, il n’existe tout simplement jamais.

function ThemeBadge() {
  // ReferenceError: window is not defined (thrown during the server render)
  const theme = window.localStorage.getItem('theme');
  return <span>{theme}</span>;
}

Le même phénomène se produit sans la moindre requête en jeu. La génération statique exécute vos composants dans Node au moment du build pour produire le HTML : un accès à window peut donc échouer pendant next build ou lors du prerendering, la stack trace apparaissant alors dans la sortie du build plutôt que dans un log serveur. SvelteKit expose même cette phase via la constante building, qui vaut true pendant le prerendering. Un composant qui, en développement, n’est rendu que côté client peut donc passer les tests locaux et malgré tout casser le build de production.

Et si le plantage provient d’une dépendance ?

Si les premières frames de la stack trace pointent vers node_modules, c’est une dépendance qui lit window au moment de l’import, et l’erreur est levée avant que le code de vos composants ne s’exécute. Les bibliothèques de graphiques, les SDK d’intégration et tout ce qui sonde le DOM à la portée du module sont les suspects habituels.

ReferenceError: window is not defined
    at node_modules/some-chart-lib/dist/index.js:12:3
    at Module._compile (node:internal/modules/cjs/loader:1358:14)

Cette distinction détermine la solution. Une erreur levée au moment de l’import se déclenche au chargement du module : encapsuler votre propre usage dans un hook de montage ne servira donc à rien, puisque le plantage survient avant même l’existence du composant. Pour ces paquets, passez directement à l’import exclusivement client de la solution trois.

Solution 1 : déplacer l’accès dans un hook de montage

La solution par défaut consiste à déplacer l’accès à window dans le hook de montage de votre framework, car les hooks de montage ne s’exécutent que dans le navigateur. La documentation de useEffect de React est explicite à ce sujet : le rendu serveur ignore les Effects, qui ne se déclenchent qu’une fois le composant arrivé dans le navigateur. Les équivalents : Vue et Nuxt utilisent onMounted, Svelte et SvelteKit utilisent onMount, qu’un composant rendu côté serveur n’appelle jamais, React Router utilise le useEffect de React, et les composants Astro placent le code navigateur dans les hooks de cycle de vie d’un îlot de framework.

import { useState, useEffect } from 'react';

function ThemeBadge() {
  const [theme, setTheme] = useState(null);

  useEffect(() => {
    setTheme(window.localStorage.getItem('theme')); // browser only
  }, []);

  return <span>{theme ?? 'default'}</span>;
}

Le serveur rend l’état de repli, le navigateur monte le composant, l’effet s’exécute et la valeur réelle vient s’insérer. Cette approche préserve intact le HTML serveur pour le reste du composant, ce qui explique qu’elle l’emporte par défaut sur les deux autres solutions.

Solution 2 : protéger avec typeof window !== ‘undefined’

Une garde typeof window !== 'undefined' est l’outil adapté pour le code au niveau module et les utilitaires partagés, là où aucun hook de cycle de vie n’est disponible.

// theme.js — a shared utility, no component lifecycle to lean on
export function getStoredTheme() {
  if (typeof window === 'undefined') return 'light'; // server fallback
  return window.localStorage.getItem('theme') ?? 'light';
}

SvelteKit propose un équivalent plus élégant avec la constante browser, et sa FAQ sur les bibliothèques côté client considère cette constante comme la manière standard de cloisonner tout ce qui touche à document ou window.

À l’intérieur du rendu d’un composant, en revanche, la garde est mal adaptée : elle amène le serveur et le navigateur à produire un HTML différent pour le même composant, échangeant un plantage contre une divergence au moment où le client prend le relais. Réservez la garde aux fonctions simples et à la portée module ; utilisez la solution une à l’intérieur des composants.

Solution 3 : exclure le composant du rendu serveur

Le dernier recours est un import dynamique exclusivement client, qui exclut totalement le composant du rendu serveur. Dans Next.js, next/dynamic avec ssr: false fait cela à l’intérieur d’un Client Component (l’option provoque une erreur dans les Server Components, il faut donc ajouter un fin wrapper 'use client'). Nuxt propose <ClientOnly>, et Astro la directive client:only.

'use client';
import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('./Chart'), {
  ssr: false,
  loading: () => <div style={{ height: 320 }} aria-hidden="true" />,
});

Mesurez le coût avant d’y recourir : le serveur n’envoie aucun HTML pour ce sous-arbre, le composant est donc absent du HTML initial, ce qui peut nuire au SEO et retarder l’interactivité. Réservez cette approche aux composants que vous ne pouvez pas modifier, principalement les dépendances qui plantent au moment de l’import.

Éviter l’effet de surgissement avec un placeholder de mêmes dimensions

Un placeholder n’empêche les décalages de mise en page que s’il occupe exactement les mêmes dimensions que le composant qu’il remplace. Rendre null côté serveur signifie que le composant apparaît de nulle part dès que le JavaScript s’exécute, repoussant vers le bas tout ce qui se trouve en dessous. Un squelette à empreinte fixe, comme le div de 320px ci-dessus, conserve l’espace jusqu’à l’arrivée du vrai balisage. Le choix entre afficher un placeholder ou null relève du même arbitrage que celui à l’origine de nombreuses divergences d’hydratation, traité en détail dans notre guide sur la résolution des erreurs d’hydratation dans Next.js. Les session replays des fallbacks exclusivement client rendent visible, sous forme de saut de mise en page, la substitution du placeholder par le contenu : c’est le moyen le plus rapide de vérifier si un placeholder correspond vraiment au balisage qu’il remplace.

Quelle solution correspond à votre cas ?

  1. Votre composant lit window dans son propre code : déplacez l’accès dans le hook de montage. Choix par défaut.
  2. Un utilitaire partagé ou une instruction au niveau module accède à window : ajoutez la garde typeof window avec une valeur de repli côté serveur.
  3. La stack trace pointe vers node_modules au moment de l’import : import dynamique exclusivement client, avec un placeholder de mêmes dimensions.
  4. L’erreur n’apparaît que dans la sortie du build : même diagnostic que ci-dessus ; la génération statique exécute exactement le même chemin de code dans Node.

Commencez par lire la stack trace

L’erreur relève de l’environnement, pas du timing : une ligne de code s’est exécutée dans Node, où window n’a jamais existé. Commencez par lire la stack trace. Si la première frame est la vôtre, un hook de montage ou une garde règle le problème tout en préservant le HTML serveur. Si elle pointe vers node_modules, isolez la dépendance derrière un import exclusivement client et donnez-lui un placeholder qui maintient la mise en page.

FAQ

L'erreur « document is not defined » est-elle le même problème que « window is not defined » ?

Oui. Les deux erreurs ont la même cause : le code s'est exécuté dans Node.js, dont la portée globale ne comprend ni window ni document. Le même diagnostic et les trois mêmes solutions s'appliquent : déplacez l'accès dans un hook de montage, protégez le code au niveau module par une vérification typeof, ou rendez le composant exclusivement côté client lorsqu'une dépendance accède au DOM au moment de l'import.

Puis-je corriger l'erreur en définissant un objet window global côté serveur ?

À éviter. Affecter un faux window à globalThis fait taire la ReferenceError, mais le serveur produit alors du balisage à partir de valeurs factices, et tout ce qui est stocké sur le polyfill est partagé entre toutes les requêtes traitées par le serveur. Cela masque également les plantages à l'import dans les dépendances au lieu de les mettre au jour. Déplacez plutôt l'accès dans un hook de montage ou derrière une garde typeof window.

Pourquoi l'erreur « window is not defined » persiste-t-elle après avoir défini ssr: false dans Next.js ?

Deux raisons courantes. Dans l'App Router, next/dynamic n'accepte ssr: false que depuis un Client Component, et Next.js lève une erreur lorsque l'option apparaît dans un Server Component : encapsulez-le donc dans un fin composant 'use client'. Par ailleurs, ssr: false n'affecte que cet import dynamique : si un autre fichier exécuté côté serveur importe statiquement la même bibliothèque, son accès à window au niveau module s'exécute toujours dans Node.

localStorage existe-t-il dans Node.js ?

Partiellement. Node embarque un global localStorage depuis la v22.4.0, sans flag depuis la v25.0.0, qui persiste jusqu'à 10 Mo dans le fichier passé via le flag --localstorage-file ; en v26, y accéder sans ce flag lève une DOMException. Sur un serveur, il n'y a derrière qu'un seul stockage pour l'ensemble du processus, et non un par visiteur ou par requête : cela n'a donc rien à voir avec le stockage par utilisateur du navigateur, et window.localStorage lève toujours une erreur puisque window lui-même n'existe jamais dans Node.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.