Avoiding Common Mistakes with Browser APIs in Next.js
One common mistake that beginners make when working with Next.js is incorrectly using browser APIs like window.localStorage
in client components. Understanding how Next.js handles server-side rendering (SSR) and client-side rendering (CSR) is crucial for avoiding this mistake. In this article, we'll explore why accessing browser APIs too early can lead to errors, and we'll cover three effective ways to handle this situation.
The Problem: Using window.localStorage
Incorrectly
In Next.js, all components (including client components) are rendered on the server side initially to generate static HTML. During this process, if a client component attempts to access browser-specific objects like window
or localStorage
, it will cause an error because these objects are not available in the Node.js environment that the server runs on.
Why Does This Happen?
Next.js pre-renders the entire page on the server, including client components, to generate the HTML that gets sent to the client. Even though client components only "hydrate" (i.e., become interactive) on the client side, they still run once on the server. When you directly use window
or localStorage
in this phase, the server doesn't know how to handle these browser-specific objects, which results in an error.
Here's an example of what not to do:
"use-client";
const MyComponent = () => {
const favorite = localStorage.getItem('favorite'); // This will cause an error on the server
return <div>{favorite ? "Favorited" : "Not Favorited"}</div>;
};
export default MyComponent;
This code will throw an error on the server because localStorage
doesn't exist in a Node.js environment.
Solutions: Handling Browser APIs Correctly
To fix this, you need to ensure that browser APIs like localStorage
are only accessed on the client side, after the component has mounted. Here are three methods to handle this properly:
1. Conditionally Check for window
You can use a condition to check whether the window
object is defined. This ensures that your code runs only on the client, not during server-side rendering.
"use-client";
const MyComponent = () => {
let favorite = false;
// Only access `localStorage` if `window` is defined (i.e., on the client side)
if (typeof window !== "undefined") {
favorite = localStorage.getItem('favorite') === 'true';
}
return <div>{favorite ? "Favorited" : "Not Favorited"}</div>;
};
export default MyComponent;
How It Works:
- The condition
typeof window !== 'undefined'
ensures thatlocalStorage
is only accessed when the component is running on the client side, preventing the error on the server.
2. Using useEffect
Hook
Another method is to use the useEffect
hook, which only runs on the client side after the initial render. Since useEffect
does not run during SSR, it’s a safe place to access browser-specific APIs like localStorage
.
"use-client";
import { useEffect, useState } from 'react';
const MyComponent = () => {
const [favorite, setFavorite] = useState(false);
useEffect(() => {
const favorite = localStorage.getItem('favorite');
setFavorite(favorite === 'true');
}, []); // Empty dependency array ensures it runs once after the component mounts
return <div>{favorite ? "Favorited" : "Not Favorited"}</div>;
};
export default MyComponent;
How It Works:
useEffect
ensures that the browser-specific code is executed only after the component has been mounted on the client. Since it doesn't run during SSR, there’s no risk of accessingwindow
orlocalStorage
on the server.
3. Using Dynamic Imports with ssr: false
You can also use Next.js’s dynamic import feature to load a component only on the client, completely skipping server-side rendering for that specific component. Setting ssr: false
ensures that the component won't even attempt to render on the server.
import dynamic from 'next/dynamic';
// Dynamically import the component with SSR disabled
const MyComponent = dynamic(() => import('./MyComponent'), { ssr: false });
export default function Page() {
return (
<div>
<h1>My Page</h1>
<MyComponent />
</div>
);
}
And inside MyComponent
:
"use-client";
const MyComponent = () => {
const favorite = localStorage.getItem('favorite');
return <div>{favorite ? "Favorited" : "Not Favorited"}</div>;
};
export default MyComponent;
How It Works:
- By setting
{ ssr: false }
, theMyComponent
will only be rendered on the client side, entirely bypassing the SSR process. This means you can safely usewindow.localStorage
without worrying about server-side errors.
Why This Happens: Understanding Hydration and SSR
Next.js pre-renders pages to generate static HTML, even for client components, in order to send it to the client quickly. After this initial render, React hydrates the page by attaching event handlers and re-rendering interactive parts. During SSR, the server can't run browser-specific APIs like localStorage
or access window
, leading to errors if you're not careful.
Conclusion
Using browser APIs like localStorage
in Next.js client components requires careful handling to avoid server-side errors. By using conditional checks, the useEffect
hook, or dynamic imports with ssr: false
, you can ensure that browser-specific code only runs on the client. Understanding how Next.js handles SSR and hydration is key to building efficient and error-free applications.
Subscribe to my newsletter
Read articles from Rishi Bakshi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Rishi Bakshi
Rishi Bakshi
Full Stack Developer with experience in building end-to-end encrypted chat services. Currently dedicated in improving my DSA skills to become a better problem solver and deliver more efficient, scalable solutions.