Hydration Proof

Search documentation

Find a page or section

ReferenceError: window is not defined

A server crash, and why the usual quick fix causes a hydration error.

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 defined

Each one names a browser global:

  • window is not defined: Next.js rendered a component or imported a module that reads window on the server.
  • document is not defined: Next.js ran code that queries or changes the page, such as document.querySelector or document.cookie.
  • localStorage is not defined: Next.js ran a storage read during render, often in a useState initializer.
  • 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 useState and useReducer initializers.
  • Module-level code in any file the page imports: a line such as const width = window.innerWidth at the top of a file runs on import.
  • Libraries that touch window when 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:

components/greeting.tsx
"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 definedHydration mismatch
WhereOn the server, before any HTML is sentIn the browser, after the HTML arrived
What you seeA server error page, a failed buildThe page renders twice, content flashes
CauseA browser global read on the serverThe server and the browser rendered different output

How to fix it

  1. 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>;
    }

    See useEffect and two-pass rendering.

  2. 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/dynamic and ssr: 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: false is not allowed in a Server Component. See next/dynamic with ssr: false, and client-only components for other frameworks.

  3. Import browser-only libraries inside an effect, with await import("…"), when you only need them after the page loads.

  4. Move module-level browser reads into functions that run in effects or event handlers.

  5. 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:

npm install -D eslint-plugin-hydration-proof

hydration-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