ReferenceError: window is not defined (Next.js, Remix or any server-rendered
React app) means code that needs the browser ran on the server, where
window does not exist. It is a server crash, not a hydration mismatch. A
typeof window check in render stops the crash but causes a mismatch. Read
browser values in an effect instead.
The error
The server throws before any HTML is sent, so the message appears in your terminal and in the framework's error page, not in the browser console:
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined
ReferenceError: self is not definedEach one names a browser global:
window is not defined: Next.js rendered a component or imported a module that readswindowon the server.document is not defined: Next.js ran code that queries or changes the page, such asdocument.querySelectorordocument.cookie.localStorage is not defined: Next.js ran a storage read during render, often in auseStateinitializer.self is not defined: usually a library bundled for browsers only.
The same crash can stop next build, when Next.js pre-renders static pages.
Why window is not defined: Next.js renders on the server
Every server-rendered page is rendered twice: once in Node.js to produce the
HTML, and once in the browser to hydrate it. Node.js has no window,
document or localStorage, so reading them during the first render throws.
"use client" does not change this. It marks a Client Component, which is
still rendered on the server for the first HTML. Only a Server Component
never runs in the browser, and only code in effects and event handlers never
runs on the server.
Three places run on the server:
- The component body, including
useStateanduseReducerinitializers. - Module-level code in any file the page imports: a line such as
const width = window.innerWidthat the top of a file runs on import. - Libraries that touch
windowwhen imported, such as some chart, map and editor packages.
Why typeof window is the wrong fix
The quick fix people reach for is a guard:
"use client";
export function Greeting() {
// Stops the crash, and causes a hydration mismatch.
const host = typeof window !== "undefined" ? window.location.hostname : "";
return <p>Hello from {host || "our site"}</p>;
}The server now renders "Hello from our site" and the browser renders "Hello from example.com". The crash becomes a hydration error: React throws the server HTML away and renders the component again. React's own error message lists this exact pattern as the first cause.
window is not defined | Hydration mismatch | |
|---|---|---|
| Where | On the server, before any HTML is sent | In the browser, after the HTML arrived |
| What you see | A server error page, a failed build | The page renders twice, content flashes |
| Cause | A browser global read on the server | The server and the browser rendered different output |
How to fix it
-
Read browser values in an effect. Effects run only in the browser, after hydration, so both renders start from the same value:
components/greeting.tsx "use client"; import { useEffect, useState } from "react"; export function Greeting() { const [host, setHost] = useState<string | null>(null); useEffect(() => { setHost(window.location.hostname); }, []); return <p>Hello from {host ?? "our site"}</p>; } -
Render a component only in the browser when it cannot run on the server at all, such as a map. In Next.js, load it with
next/dynamicandssr: false, from a Client Component:app/store-locator/map-loader.tsx "use client"; import dynamic from "next/dynamic"; const Map = dynamic(() => import("./map"), { ssr: false, loading: () => <p>Loading map…</p>, }); export function MapLoader() { return <Map />; }ssr: falseis not allowed in a Server Component. See next/dynamic with ssr: false, and client-only components for other frameworks. -
Import browser-only libraries inside an effect, with
await import("…"), when you only need them after the page loads. -
Move module-level browser reads into functions that run in effects or event handlers.
-
Read what the server can know on the server. A theme, a language or a signed-in user can come from a cookie, so the first render needs no browser value. See storage.
Find every instance
The ESLint plugin finds both the crash and the mismatch before you run the app:
no-browser-global-in-renderreportswindow,document,navigatorandlocationread during render.no-storage-in-initial-renderreportslocalStorageandsessionStorage.no-window-render-branchreports thetypeof windowguard in render.no-client-only-initial-statereports browser values inuseStateinitializers.
npm install -D eslint-plugin-hydration-proofhydration-proof loads every route: a page whose server render crashed is
reported as HP9005 (an error status), or
HP2006 when the crash was inside a Suspense boundary
that React then rendered in the browser. The mismatch a typeof window guard
leaves behind is reported with the cause
browser-only API used during render:
npx hydration-proof test