12k
All articles

Cómo solucionar 'window is not defined' en aplicaciones renderizadas en el servidor

Corrige window is not defined en apps renderizadas en servidor con hooks de montaje, guardas typeof window e imports solo cliente para dependencias.

OpenReplay Team
OpenReplay Team
Cómo solucionar 'window is not defined' en aplicaciones renderizadas en el servidor

El error window is not defined significa que tu código se ejecutó en Node.js, donde no existe ningún objeto window: los frameworks con renderizado en servidor ejecutan tus componentes primero en el servidor, antes de que intervenga cualquier navegador.

El error suele aparecer justo después de añadir renderizado en servidor a una aplicación que funcionaba, o al mover un componente que funcionaba bien en el cliente a Next, Nuxt, SvelteKit, Astro o React Router. El componente no cambió. Lo que cambió es dónde se ejecuta, y el stack trace te indica cuál de las tres soluciones siguientes necesitas.

Puntos clave

  • window is not defined significa que el código se ejecutó en Node.js, donde window no existe en ningún momento ni en ningún ciclo de vida; no es un problema de temporización.
  • La solución por defecto es mover el acceso a un hook de montaje (useEffect, onMounted, onMount), porque los hooks de montaje nunca se ejecutan en el servidor.
  • Una guarda typeof window !== 'undefined' corresponde al código a nivel de módulo y a las utilidades compartidas; dentro del render de un componente hace que el HTML del servidor y del cliente divergan.
  • El renderizado exclusivo en cliente es el último recurso: elimina el componente por completo del HTML generado por el servidor.
  • El mismo fallo puede ocurrir durante el build, porque la generación estática ejecuta los componentes en Node para producir HTML.

¿Por qué ocurre ‘window is not defined’ en aplicaciones renderizadas en el servidor?

Las aplicaciones con renderizado en servidor ejecutan tus componentes dos veces: una en Node.js para producir HTML y otra en el navegador. El ámbito global de Node.js no incluye window ni document, por lo que cualquier código que los toque durante la pasada del servidor lanza un ReferenceError. El objeto no está “aún no disponible”; en Node simplemente nunca existe.

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

Lo mismo aplica sin que haya ninguna petición en juego. La generación estática ejecuta tus componentes en Node en tiempo de build para producir HTML, por lo que un acceso a window puede fallar durante next build o el prerenderizado, con el stack trace apareciendo en la salida del build en lugar de en un log del servidor. SvelteKit incluso expone esta fase mediante la constante building, que es true durante el prerenderizado. Por tanto, un componente que en desarrollo solo se renderiza en el cliente puede superar las pruebas locales y aun así romper el build de producción.

¿Qué pasa si el fallo está en una dependencia?

Si los primeros frames del stack trace apuntan a node_modules, es que una dependencia está leyendo window en tiempo de importación, y lanza el error antes de que se ejecute cualquier parte del código de tu componente. Las librerías de gráficos, los SDK de embeds y todo lo que sondee el DOM en el ámbito del módulo son los sospechosos habituales.

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)

Esta distinción determina la solución. Un error lanzado en tiempo de importación se dispara cuando se carga el módulo, así que envolver tu propio uso en un hook de montaje no sirve de nada; el fallo ocurre antes de que el componente exista. Para estos paquetes, salta directamente a la importación exclusiva de cliente de la solución tres.

Solución 1: Mueve el acceso a un hook de montaje

La solución por defecto es mover el acceso a window al hook de montaje de tu framework, porque los hooks de montaje solo se ejecutan en el navegador. La referencia de useEffect de React es explícita al respecto: el render en servidor omite los Effects, y estos se disparan solo cuando el componente llega al navegador. Los equivalentes: Vue y Nuxt usan onMounted; Svelte y SvelteKit usan onMount, que un componente renderizado en el servidor nunca llama; React Router usa el useEffect de React; y los componentes de Astro colocan el código de navegador en los hooks de ciclo de vida de una isla 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>;
}

El servidor renderiza el estado de reserva, el navegador monta el componente, se ejecuta el effect y se rellena el valor real. Así se mantiene intacto el HTML del servidor para el resto del componente, y por eso esta opción es preferible a las otras dos como solución por defecto.

Solución 2: Protege con typeof window !== ‘undefined’

Una guarda typeof window !== 'undefined' es la herramienta adecuada para código a nivel de módulo y utilidades compartidas, donde no hay ningún hook de ciclo de vida 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 ofrece un equivalente más limpio con la constante browser, y sus preguntas frecuentes sobre librerías del lado del cliente tratan esa constante como la forma estándar de aislar cualquier cosa que toque document o window.

Dentro del render de un componente, sin embargo, la guarda encaja mal: hace que el servidor y el navegador produzcan HTML distinto para el mismo componente, cambiando un fallo por un desajuste cuando el cliente toma el control. Reserva la guarda para funciones planas y el ámbito de módulo; usa la solución uno dentro de los componentes.

Solución 3: Omite el renderizado en servidor para el componente

El último recurso es una importación dinámica exclusiva de cliente, que excluye el componente del renderizado en servidor por completo. En Next.js, next/dynamic con ssr: false hace esto dentro de un Client Component (da error en Server Components, así que añade un envoltorio ligero con 'use client'). Nuxt tiene <ClientOnly>, y Astro tiene la directiva client:only.

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

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

Ten claro el coste antes de recurrir a esto: el servidor no envía HTML para ese subárbol, por lo que el componente está ausente del HTML inicial, lo que puede perjudicar el SEO y retrasar la interactividad. Resérvalo para componentes que no puedas modificar, principalmente dependencias que lanzan errores en tiempo de importación.

Evita el “pop-in” con un placeholder de la misma forma

Un placeholder solo evita el desplazamiento del layout si ocupa las mismas dimensiones que el componente al que sustituye. Renderizar null en el servidor significa que el componente aparece de la nada en cuanto se ejecuta JavaScript, empujando hacia abajo todo lo que está debajo. Un skeleton de huella fija, como el div de 320 px de arriba, reserva el espacio hasta que llega el marcado real. Decidir si renderizar un placeholder o null responde al mismo compromiso que está detrás de muchos desajustes de hidratación, tratado en profundidad en nuestra guía para solucionar errores de hidratación en Next.js. Las repeticiones de sesión de los fallbacks exclusivos de cliente hacen visible el cambio de placeholder a contenido como un salto de layout, que es la forma más rápida de comprobar si un placeholder realmente coincide con el marcado que reemplaza.

¿Qué solución encaja en tu caso?

  1. Tu componente lee window en su propio código: mueve el acceso al hook de montaje. Opción por defecto.
  2. Una utilidad compartida o una instrucción a nivel de módulo toca window: añade la guarda typeof window con un valor de reserva para el servidor.
  3. El stack trace apunta a node_modules en tiempo de importación: importación dinámica exclusiva de cliente, con un placeholder de la misma forma.
  4. El error aparece solo en la salida del build: el mismo triaje que arriba; la generación estática ejecuta exactamente la misma ruta de código en Node.

Lee primero el stack trace

El error es un problema de entorno, no de temporización: alguna línea de código se ejecutó en Node, donde window nunca ha existido. Lee primero el stack trace. Si el frame superior es tuyo, un hook de montaje o una guarda lo resuelven manteniendo el HTML del servidor. Si apunta a node_modules, aísla la dependencia detrás de una importación exclusiva de cliente y dale un placeholder que sostenga el layout.

Preguntas frecuentes

¿'document is not defined' es el mismo problema que 'window is not defined'?

Sí. Ambos errores tienen la misma causa: el código se ejecutó en Node.js, cuyo ámbito global no incluye ni window ni document. Se aplican el mismo triaje y las mismas tres soluciones, así que mueve el acceso a un hook de montaje, protege el código a nivel de módulo con una comprobación typeof, o renderiza el componente solo en el cliente cuando una dependencia toca el DOM en tiempo de importación.

¿Puedo solucionar el error definiendo un objeto window global en el servidor?

Evítalo. Asignar un window falso a globalThis silencia el ReferenceError, pero entonces el servidor renderiza marcado a partir de valores falsos, y todo lo que se almacene en el polyfill se comparte entre todas las peticiones que atiende el servidor. Además, oculta los fallos en tiempo de importación de las dependencias en lugar de sacarlos a la luz. Mueve el acceso a un hook de montaje o detrás de una guarda typeof window.

¿Por qué sigue apareciendo 'window is not defined' después de establecer ssr: false en Next.js?

Hay dos razones habituales. En el App Router, next/dynamic acepta ssr: false solo desde un Client Component, y Next.js lanza un error cuando la opción aparece en un Server Component, así que envuélvelo en un componente ligero con 'use client'. Además, ssr: false solo afecta a esa importación dinámica: si otro archivo ejecutado en el servidor importa la misma librería de forma estática, su acceso a window a nivel de módulo se sigue ejecutando en Node.

¿Existe localStorage en Node.js?

Parcialmente. Node incluye un global localStorage desde la v22.4.0, sin flag desde la v25.0.0, que persiste hasta 10 MB en el archivo indicado mediante el flag --localstorage-file; en la v26, acceder a él sin ese flag lanza una DOMException. En un servidor hay un único almacén detrás para todo el proceso, no uno por visitante ni por petición, así que no tiene nada que ver con el almacenamiento por usuario del navegador, y window.localStorage sigue lanzando error porque window en sí nunca existe en 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.