Ceasefire now! 🕊️🇵🇸

Why are document, window, localStorage and sessionStorage undefined in client components?

I'm having this function in a client component

"use client";

export default function Page() {
  const [theme, setTheme] = useState(localStorage.getItem("theme"));
  return ...;
}

But I'm getting an error saying ReferenceError: localStorage is not defined.

Why is that? Isn't it supposed to work in client components?

The name "client component" is actually quite misleading. It's not a component that exclusively runs in the browser. It's a component that runs on the server and the client. The code inside the component is executed first on the server and the client or in other words, it's prerendered on the server and then hydrated on the client.

During that initial prerendering, since the component is rendered on the server, it doesn't have access to the browser APIs named above. Hence you got that error.

There are 2 fixes for this problem:

  • move logic using browser-specific APIs to useEffect (or an event handler, or any place that only runs on the client)
  • use the component dynamically with next/dynamic and ssr: false

Moving logic to useEffect

If we move the logic inside useEffect hook, it will only run on the client. So, we can use these APIs without any problem.

"use client";

export default function Page() {
  const [theme, setTheme] = useState<string | null>(null);

  useEffect(() => {
    setTheme(localStorage.getItem("theme"));
  }, []);

  if (!theme) return <div>Theme is loading</div>;
  return <div>Theme is {theme}</div>;
}

Note that if you do it this way, as you can see you need a loading state, which may cause a flash of unstyled content (FOUC). To overcome it, you have to use a <script> (or next/script) to get the value before hydration and configure styling based on that using vanilla JavaScript. For theming specifically, we recommend next-themes which handles this problem very well, so you don't have to touch all this <script> and useEffect shenanigans.

By the way, there are already plenty of existing solutions made for many of these problems. For example, for localStorage handling, you can have useLocalStorage.

Using next/dynamic with ssr: false

If we import a component dynamically with ssr: false, it will only run on the client, at the cost of the content not being available in the initial HTML (so crawlers may not be able to see it). If that is fine for you, you can proceed to use these APIs without any problem.

import dynamic from "next/dynamic";

const Content = dynamic(() => import("./content.tsx"), { ssr: false });

export default function Page() {
  return <Content />;
}
This site is NOT an official Next.js or Vercel website. Learn more.
Updated:
Authors: