Blog
3 min read

"ReferenceError: window is not defined" in Next.js: How to Fix It

window, document and localStorage only exist in the browser. Next.js renders components on the server first, where they don't exist. Why it happens, and the fixes: useEffect, 'use client', typeof checks, dynamic imports with ssr: false, and libraries that touch window on import.

ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined

window, document, localStorage, navigator — these exist only in the browser. Frameworks like Next.js run your components on the server first to produce HTML, and on the server (Node.js) there is no window. (SSR vs CSR vs SSG)

So code that touches window while rendering crashes during server rendering or the build.

Fix 1: use it inside useEffect

useEffect only runs in the browser, after the component has mounted:

'use client'
import { useEffect, useState } from 'react'

export function ThemeToggle() {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    setTheme(localStorage.getItem('theme') ?? 'light')   // ✅ browser only
  }, [])

  // ...
}

(React useEffect explained)

Event handlers (onClick, etc.) also only run in the browser, so window is safe there.

Fix 2: check before using

For utility code that might run in either place:

if (typeof window !== 'undefined') {
  window.scrollTo(0, 0)
}

typeof is safe even when the variable doesn't exist; if (window) would itself throw.

Don't use this check to render different output on server and client:

return <p>{typeof window !== 'undefined' ? 'Browser' : 'Server'}</p>   // ❌

That makes the server HTML differ from the browser's first render, causing a hydration error. Use useEffect for anything that changes what's displayed. (Next.js hydration errors)

Fix 3: "use client" isn't enough on its own

In the App Router, adding "use client" makes a component interactive in the browser — but client components are still rendered on the server once for the initial HTML. So window in the component body still fails. You still need useEffect (or an event handler). (React Server Components explained)

Fix 4: load a component only in the browser

Some libraries — maps, charts, rich-text editors, anything that touches window as soon as it's imported — can't be server-rendered at all. Load them dynamically with SSR turned off:

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

const Map = dynamic(() => import('./Map'), { ssr: false })

export default function Page() {
  return <Map />
}

In the App Router, ssr: false must be used inside a client component.

Fix 5: import browser-only libraries lazily

If a library only needs to run on an action:

async function exportPdf() {
  const { jsPDF } = await import('jspdf')   // loaded in the browser on click
  // ...
}

In other environments

The same error appears in:

  • Node.js scripts and API routes — there's no browser at all. Move the code to the frontend.
  • Tests using Node rather than a browser-like environment — configure Vitest or Jest with jsdom or happy-dom. (Vitest vs Jest)
  • Web workers — no window or document there either (use self).

Quick reference

Where your code runs Safe to use window?
Component body (server or client component) ❌
useEffect / useLayoutEffect ✅
Event handlers ✅
Server components, API routes, middleware ❌
dynamic(..., { ssr: false }) component ✅

EasySpawn builds and runs your Next.js app on a real server, so server-rendering errors show up in development exactly as they would in production. See how it works or join the waitlist.

Related: Next.js Hydration Errors · React Server Components Explained · What Is Next.js? · localStorage vs sessionStorage vs Cookies

Keep reading