Corrigindo 'window is not defined' em Aplicações Renderizadas no Servidor
Corrija window is not defined em apps renderizadas no servidor com hooks de montagem, guards typeof window e imports apenas no cliente.
O erro window is not defined significa que seu código foi executado no Node.js, onde não existe nenhum objeto window: frameworks com renderização no servidor executam seus componentes primeiro no servidor, antes de qualquer navegador entrar em cena.
O erro normalmente aparece logo depois que você adiciona renderização no servidor a uma aplicação que funcionava, ou move um componente que funcionava bem no client-side para Next, Nuxt, SvelteKit, Astro ou React Router. O componente não mudou. O que mudou foi onde ele é executado, e o stack trace indica qual das três correções abaixo você precisa.
Pontos Principais
window is not definedsignifica que o código foi executado no Node.js, ondewindownunca existe em nenhum momento de nenhum ciclo de vida; não é um problema de timing.- A correção padrão é mover o acesso para um hook de montagem (
useEffect,onMounted,onMount), porque hooks de montagem nunca são executados no servidor. - Uma verificação
typeof window !== 'undefined'pertence a código em nível de módulo e utilitários compartilhados; dentro do render de um componente, ela faz o HTML do servidor e do cliente divergirem. - Renderização exclusivamente no cliente é o último recurso: ela remove o componente do HTML do servidor por completo.
- O mesmo crash pode ocorrer durante o build, porque a geração estática executa componentes no Node para produzir HTML.
Por Que ‘window is not defined’ Acontece em Aplicações Renderizadas no Servidor?
Aplicações renderizadas no servidor executam seus componentes duas vezes: uma no Node.js para produzir o HTML e outra no navegador. O escopo global do Node.js não inclui window nem document, então qualquer código que os acesse durante a passagem no servidor lança um ReferenceError. O objeto não está “ainda indisponível”; no Node ele simplesmente nunca existe.
function ThemeBadge() {
// ReferenceError: window is not defined (thrown during the server render)
const theme = window.localStorage.getItem('theme');
return <span>{theme}</span>;
}
O mesmo se aplica sem nenhuma requisição envolvida. A geração estática executa seus componentes no Node em tempo de build para produzir HTML, então um acesso a window pode falhar durante o next build ou na pré-renderização, com o stack trace aparecendo na saída do build em vez de em um log do servidor. O SvelteKit chega a expor essa fase por meio da constante building, que é true durante a pré-renderização. Um componente que, em desenvolvimento, só é renderizado no cliente pode, portanto, passar nos testes locais e ainda assim quebrar o build de produção.
E Se o Crash Estiver em uma Dependência?
Se os frames no topo do stack trace apontarem para dentro de node_modules, é uma dependência lendo window em tempo de import, e ela lança o erro antes de qualquer código de componente seu ser executado. Bibliotecas de gráficos, SDKs de embed e qualquer coisa que inspecione o DOM em escopo de módulo são os suspeitos habituais.
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)
Essa distinção define a correção. Um erro lançado em tempo de import ocorre quando o módulo é carregado, então envolver o seu próprio uso em um hook de montagem não ajuda; o crash acontece antes de o componente existir. Para esses pacotes, vá direto para o import exclusivo do cliente na correção três.
Correção 1: Mova o Acesso Para um Hook de Montagem
A correção padrão é mover o acesso a window para o hook de montagem do seu framework, porque hooks de montagem só são executados no navegador. A referência do useEffect do React é explícita quanto a isso: o render no servidor ignora os Effects, e eles só disparam quando o componente chega ao navegador. Os equivalentes: Vue e Nuxt usam onMounted, Svelte e SvelteKit usam onMount, que um componente renderizado no servidor nunca chama, React Router usa o useEffect do React, e componentes Astro colocam código de navegador nos hooks de ciclo de vida de uma island 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>;
}
O servidor renderiza o estado de fallback, o navegador monta, o effect é executado e o valor real é preenchido. Isso mantém intacto o HTML gerado no servidor para o resto do componente, e é por isso que essa abordagem supera as outras duas como escolha padrão.
Correção 2: Proteja com typeof window !== ‘undefined’
Uma verificação typeof window !== 'undefined' é a ferramenta correta para código em nível de módulo e utilitários compartilhados, onde não há nenhum hook de ciclo de vida disponível.
// 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';
}
O SvelteKit oferece um equivalente mais limpo na constante browser, e seu FAQ sobre bibliotecas client-side trata essa constante como a forma padrão de isolar qualquer coisa que toque document ou window.
Dentro do render de um componente, porém, a verificação não é adequada: ela faz o servidor e o navegador produzirem HTML diferente para o mesmo componente, trocando um crash por um mismatch quando o cliente assume o controle. Mantenha a verificação em funções simples e em escopo de módulo; use a correção um dentro de componentes.
Correção 3: Ignore a Renderização no Servidor para o Componente
O último recurso é um import dinâmico exclusivo do cliente, que exclui o componente da renderização no servidor por completo. No Next.js, next/dynamic com ssr: false faz isso dentro de um Client Component (ele gera erro em Server Components, então adicione um wrapper fino com 'use client'). O Nuxt tem o <ClientOnly>, e o Astro tem a diretiva client:only.
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
ssr: false,
loading: () => <div style={{ height: 320 }} aria-hidden="true" />,
});
Deixe claro o custo antes de recorrer a isso: o servidor não envia HTML para aquela subárvore, então o componente está ausente do HTML inicial, o que pode prejudicar o SEO e atrasar a interatividade. Reserve essa abordagem para componentes que você não pode alterar, principalmente dependências que lançam erro em tempo de import.
Evite o “Pop-In” Com um Placeholder de Mesmas Dimensões
Um placeholder só evita layout shift se ocupar as mesmas dimensões do componente que ele substitui. Renderizar null no servidor significa que o componente aparece do nada assim que o JavaScript é executado, empurrando tudo o que está abaixo dele para baixo na página. Um skeleton com dimensões fixas, como o div de 320px acima, reserva o espaço até a marcação real chegar. Decidir se deve renderizar um placeholder ou null envolve o mesmo trade-off que está por trás de muitos mismatches de hidratação, abordado em detalhes no nosso guia sobre como corrigir erros de hidratação no Next.js. Session replays de fallbacks exclusivos do cliente tornam a troca de placeholder para conteúdo visível como um salto de layout, que é a forma mais rápida de verificar se um placeholder realmente corresponde à marcação que ele substitui.
Qual Correção Se Aplica ao Seu Caso?
- Seu componente lê
windowno próprio código: mova o acesso para o hook de montagem. Escolha padrão. - Um utilitário compartilhado ou uma instrução em nível de módulo acessa
window: adicione a verificaçãotypeof windowcom um valor de fallback para o servidor. - O stack trace aponta para dentro de
node_modulesem tempo de import: import dinâmico exclusivo do cliente, com um placeholder de mesmas dimensões. - O erro aparece apenas na saída do build: a mesma triagem acima se aplica; a geração estática executa exatamente o mesmo caminho de código no Node.
Leia o Stack Trace Primeiro
O erro é um problema de ambiente, não de timing: alguma linha de código foi executada no Node, onde window nunca existiu. Leia o stack trace primeiro. Se o frame do topo é seu, um hook de montagem ou uma verificação resolvem o problema mantendo o HTML do servidor. Se ele aponta para dentro de node_modules, isole a dependência atrás de um import exclusivo do cliente e dê a ela um placeholder que sustente o layout.
Perguntas Frequentes
'document is not defined' é o mesmo problema que 'window is not defined'?
Sim. Os dois erros têm a mesma causa: o código foi executado no Node.js, cujo escopo global não inclui nem window nem document. A mesma triagem e as mesmas três correções se aplicam, então mova o acesso para um hook de montagem, proteja o código em nível de módulo com uma verificação typeof, ou renderize o componente apenas no cliente quando uma dependência tocar o DOM em tempo de import.
Posso corrigir o erro definindo um objeto window global no servidor?
Evite isso. Atribuir um window falso ao globalThis silencia o ReferenceError, mas o servidor então renderiza marcação a partir de valores falsos, e qualquer coisa armazenada nesse polyfill é compartilhada entre todas as requisições que o servidor atende. Isso também esconde crashes em tempo de import em dependências em vez de expô-los. Prefira mover o acesso para um hook de montagem ou colocá-lo atrás de uma verificação typeof window.
Por que 'window is not defined' continua aparecendo depois de definir ssr: false no Next.js?
Dois motivos comuns. No App Router, o next/dynamic aceita ssr: false apenas a partir de um Client Component, e o Next.js gera um erro quando a opção aparece em um Server Component, então envolva-o em um componente fino com 'use client'. Além disso, ssr: false afeta apenas aquele import dinâmico: se outro arquivo executado no servidor importar a mesma biblioteca de forma estática, o acesso a window em nível de módulo dela ainda será executado no Node.
localStorage existe no Node.js?
Parcialmente. O Node passou a incluir um global localStorage desde a v22.4.0, sem flag a partir da v25.0.0, que persiste até 10 MB no arquivo informado pela flag --localstorage-file; na v26, acessá-lo sem essa flag lança uma DOMException. Em um servidor há um único store por trás dele para todo o processo, não um por visitante ou por requisição, então não se parece em nada com o armazenamento por usuário do navegador, e window.localStorage ainda lança erro porque o próprio window nunca existe no Node.