Fixing Hydration Mismatch in Next.js Using useEffect
Adam C. |

In Next.js, hydration errors are a common issue when using browser-specific APIs, such as sessionStorage or localStorage. One of the most frequent errors developers encounter is:

Error: Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "2023-2024" Client: "2022-2023"

This error occurs because Next.js renders content on both the server and the client. When the server renders the content, it doesn’t have access to the browser-specific APIs, so it may use default values. However, once the content is hydrated on the client side, these browser APIs become available, and the content may change, leading to a mismatch between the server-rendered and client-rendered HTML.

Photo by Janosch Lino on Unsplash

In this post, we’ll walk through how to use the useEffect hook to fix this hydration mismatch by ensuring that browser-specific logic, such as accessing sessionStorage, only runs on the client.

Understanding the Problem

Let's say you have the following code that uses sessionStorage to save and retrieve the active season of a sports application:

const [activeSeason, setActiveSeason] = useState(initialSeason());

function initialSeason() {
  if (typeof window !== "undefined") {
    const savedSeason = sessionStorage.getItem("SS-IM-ACTIVE-SEASON");
    if (savedSeason) {
      return JSON.parse(savedSeason); // Retrieve both season and seasonDisplay
    }
  }
  return {
    season: season,
    seasonDisplay: `${seasonStart}-${seasonEnd}`, // Default seasonDisplay
    seasonStart: seasonStart,
    seasonEnd: seasonEnd,
  };
}

At first glance, this looks fine, but it will cause a hydration error when the server renders one set of values (e.g., the default season) and the client renders a different value from sessionStorage. Since the HTML differs, Next.js will throw a hydration mismatch error.

Solution: Using useEffect

The key to solving this issue is to ensure that any browser-specific logic, such as accessing sessionStorage, only runs on the client side after the component has mounted. This is where useEffect comes in handy.

useEffect allows you to delay certain logic until after the component has been rendered on the client, ensuring that the server-rendered HTML matches the initial client-side HTML. Let’s modify the previous code to use useEffect and prevent the hydration mismatch.

Fixing the Hydration Mismatch

Here’s how to refactor the code:

import { useState, useEffect } from 'react';

const MyComponent = ({ season, seasonStart, seasonEnd }) => {
  // Initialize state with the default season for server-side rendering
  const [activeSeason, setActiveSeason] = useState({
    season: season,
    seasonDisplay: `${seasonStart}-${seasonEnd}`,
    seasonStart: seasonStart,
    seasonEnd: seasonEnd,
  });

  useEffect(() => {
    // Run only on the client side
    if (typeof window !== "undefined") {
      const savedSeason = sessionStorage.getItem("SS-IM-ACTIVE-SEASON");
      if (savedSeason) {
        const parsedSeason = JSON.parse(savedSeason);
        setActiveSeason(parsedSeason); // Update state with sessionStorage data
      }
    }
  }, []); // Empty dependency array ensures this runs only after the component mounts

  return (
    <div>
      <h1>{activeSeason.seasonDisplay}</h1>
    </div>
  );
};

export default MyComponent;

Explanation:

  1. Default State for Server-Side Rendering:
    When initializing the useState hook, we provide a default value for the active season based on props like season, seasonStart, and seasonEnd. This ensures that the server-rendered HTML will always have a consistent value.
  2. useEffect for Client-Side Update:
    The useEffect hook is used to access sessionStorage only on the client side. Since useEffect runs after the component has mounted, it avoids any mismatch during the initial render.
  3. Preventing Hydration Mismatch:
    By setting the default state during server-side rendering and only updating the state after the component has mounted, we ensure that the server-rendered and client-rendered HTML match, avoiding the hydration mismatch error.

Why This Works

Next.js pre-renders the page on the server and sends the HTML to the client. When the page is loaded on the client, React re-renders the component to "hydrate" it, attaching the React event listeners to the HTML. If the HTML on the server and client are different, a mismatch occurs.

By using useEffect, the sessionStorage logic is deferred until after the component is mounted on the client side. This ensures the server and client initial renders are consistent, and only after the initial render will the state be updated based on sessionStorage.

Key Takeaways

  • Server-Side Rendering (SSR) Mismatch: This error occurs when the content rendered on the server differs from what’s rendered on the client.
  • Fix with useEffect: By using the useEffect hook, you can ensure that browser-specific APIs like sessionStorage are only accessed on the client side, preventing hydration mismatches.
  • Best Practice: Always ensure that initial state values are consistent for both server-side and client-side rendering, and only modify the state after the client has fully mounted.

With this approach, you can ensure that your Next.js application works seamlessly with browser APIs like sessionStorage without causing any hydration mismatch issues.