afaqrashid.com  ·  portfolio · 2026 ed.

"window is not defined" in Next.js (Even With "use client"): Tested Fixes

"use client" doesn't mean "runs only in the browser". Client components are also rendered on the server, to produce the first HTML, and the server has no window, document or localStorage. Any of those read during render, or when a file is imported, fails there.

I reproduced five versions of it on Next.js 16.4.4 (Node 26), then built four fixes and checked them in a browser.

Where the error comes from

code                                              next build result
window.innerWidth in a client component's render  ReferenceError: window is not defined
localStorage.getItem() in render                  TypeError: Cannot read properties of undefined (reading 'getItem')
const host = window.location.host at file level   ReferenceError: window is not defined
document.title in a server component              ReferenceError: document is not defined
a library that reads window when imported         ReferenceError: window is not defined

Every one compiled, then failed with "Error occurred prerendering page "/w-render"" and "Export encountered an error on /w-render/page, exiting the build." That's the message to search your build log for.

The localStorage case gave a different error from what most answers online show. On Node 26, localStorage existed as a global name on the server but its value was undefined (recent Node versions add Web Storage, which needs a storage file to work), so instead of "localStorage is not defined" I got "Cannot read properties of undefined (reading 'getItem')". On older Node you get the ReferenceError. Same cause, same fix.

The file-level case catches people who did move their code into useEffect: the effect was fine, but const host = window.location.host sat at the top of the file and ran as soon as the server imported it. The library case is the same thing in someone else's code. Map libraries such as Leaflet are the classic example.

Fix 1: read it in useEffect

"use client";
import { useEffect, useState } from "react";

export default function Theme() {
  const [theme, setTheme] = useState<string | null>(null);
  useEffect(() => setTheme(localStorage.getItem("theme") ?? "light"), []);
  return <p>theme {theme ?? "…"}</p>;
}

useEffect only runs in the browser. The prerendered HTML had the placeholder, the browser showed "theme light", and the console was clean. Don't use typeof window !== "undefined" in render instead: the server and browser then render different things, which is a hydration error (I measured that in the hydration post linked below).

Fix 2: useSyncExternalStore for values that change

"use client";
import { useSyncExternalStore } from "react";

const subscribe = (cb: () => void) => {
  window.addEventListener("resize", cb);
  return () => window.removeEventListener("resize", cb);
};

export default function Width() {
  const width = useSyncExternalStore(subscribe, () => window.innerWidth, () => 0);
  return <p>width {width || "…"}</p>;
}

The third argument is the value used on the server and during hydration, so there's no mismatch. The page built, the HTML had the placeholder, and the browser showed "width 1710" with no console errors. This is the cleaner choice for window size, online status or anything else you subscribe to.

Fix 3: load browser-only libraries with next/dynamic and ssr: false

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

const Map = dynamic(() => import("../components/Map"), {
  ssr: false,
  loading: () => <p>loading map…</p>,
});

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

Put the library import inside components/Map.tsx, and import that component only through dynamic(). The server HTML had "loading map…" and the browser replaced it with the map. The call has to be in a client component: in a server component the build failed with "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."

Fix 4: import the library inside useEffect

useEffect(() => {
  import("leaflet").then((L) => {
    // create the map here
  });
}, []);

A dynamic import() inside an effect only loads the library in the browser. It built and worked the same as Fix 3, and needs no wrapper component, which makes it handy when you only need one function from the library.

Server components

A server component never runs in a browser, so window and document can't work there at all. If you need them, move that part into a small client component and use one of the fixes above.

Checklist

  • Search the build log for "Error occurred prerendering page": it names the page.
  • Look for window, document and localStorage outside useEffect and event handlers, including at the top of files.
  • Browser-only library: next/dynamic with ssr: false from a client component, or import() inside useEffect.
  • Values that change (window size): useSyncExternalStore with a server snapshot.
  • Avoid typeof window checks in render: they trade this error for a hydration error.

Related

Sources