Avoiding Common Mistakes with Browser APIs in Next.js

Rishi BakshiRishi Bakshi
4 min read

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 that localStorage 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 accessing window or localStorage 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 }, the MyComponent will only be rendered on the client side, entirely bypassing the SSR process. This means you can safely use window.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.

11
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.