Skip to main content

Command Palette

Search for a command to run...

Understanding Hydration in Next.js & Fixing Hydration Errors

Published
4 min read
Understanding Hydration in Next.js & Fixing Hydration Errors
N

Product Development as an Expertise Since 2015 Founded in August 2015, we are a USA-based Bespoke Engineering Studio providing Product Development as an Expertise. With 80+ satisfied clients worldwide, we serve startups and enterprises across San Francisco, Seattle, New York, London, Pune, Bangalore, Tokyo and other prominent technology hubs.

🔍 What is Hydration & Why is it Important?

🧪 Hydration is the process where client-side JavaScript takes over the server-rendered HTML to make it interactive. This ensures React can “attach” event listeners to the existing HTML without recreating it.

⚡ In Next.js, hydration allows faster page loads as the initial HTML is generated server-side (SSR). However, the client and server DOM must match exactly. If they don’t, hydration errors occur, disrupting the user experience.

Why It Matters: Hydration combines the performance of SSR with the interactivity of React. Debugging hydration issues is critical to maintaining these benefits.

🛠️ Hydration Errors: An Example

Imagine creating a tabbed interface. Here’s a generic example:

🚩 Problematic Code

"use client";
import React, { useState, useEffect } from "react";
import TabLayout from "@/src/components/tab-layout";
const TabbedInterface: React.FC = () => {
  const tabs = [
    { value: "tab1", label: "Tab 1" },
    { value: "tab2", label: "Tab 2" },
    { value: "tab3", label: "Tab 3" },
  ];
  const tabContent = {
    tab1: <div>Content for Tab 1</div>,
    tab2: <div>Content for Tab 2</div>,
    tab3: <div>Content for Tab 3</div>,
  };
  const getInitialTab = () => {
    if (typeof window !== "undefined") {
      const hash = window.location.hash.replace("#", "");
      return tabs.some((tab) => tab.value === hash) ? hash : "tab1";
    }
    return "tab1";
  };
  const [activeTab, setActiveTab] = useState(getInitialTab());
  const handleTabChange = (tabValue: string) => {
    setActiveTab(tabValue);
    window.history.replaceState(null, "", `#${tabValue}`);
  };
  useEffect(() => {
    const handleHashChange = () => {
      const hash = window.location.hash.replace("#", "");
      if (tabs.some((tab) => tab.value === hash)) {
        setActiveTab(hash);
      }
    };
    window.addEventListener("hashchange", handleHashChange);
    return () => {
      window.removeEventListener("hashchange", handleHashChange);
    };
  }, []);
  return (
    <div className="space-y-5 h-full">
      <TabLayout tabs={tabs} tabContent={tabContent} defaultTab={activeTab} onTabChange={handleTabChange} />
    </div>
  );
};
export default TabbedInterface;

❓ Looks Correct, But What About the Browser?

The above code may appear fine, but it can lead to:

Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.

💡 Why This Happens

The server-rendered HTML differs from what React renders during hydration due to:

  1. 🌐 Client-Side Only Logic
  • getInitialTab uses window.location, available only on the client. The server defaults to "tab1", but the client may derive a different value, causing a mismatch.

2. 🧩 Tab Mismatch

  • Tabs component’s defaultValue may not match the dynamically updated activeTab after hydration.

3 . ️ Hash Changes

  • The useEffect hook adjusts activeTab based on window.location.hash, but this happens post-hydration, leading to transient mismatches.

🔧 How to Fix the Hydration Issue

🛠️ Solution 1: Initialize State After Hydration

Ensure activeTab initializes consistently on both server and client:

const [activeTab, setActiveTab] = useState("tab1");

useEffect(() => {
  const hash = window.location.hash.replace("#", "");
  if (tabs.some((tab) => tab.value === hash)) {
    setActiveTab(hash);
  }
}, []);

🛠️ Solution 2: Controlled Tab State

Use activeTab as a controlled value in TabLayout to ensure consistency:

<TabLayout
  tabs={tabs}
  tabContent={tabContent}
  activeTab={activeTab}
  onTabChange={handleTabChange}
/>

🛠️ Solution 3: Avoid SSR Logic with window

Avoid using browser-specific APIs like window during SSR. For example:

const getInitialTab = () => "tab1";

✅ Final Working Code

Tabbed Interface Component

"use client";
import React, { useState, useEffect } from "react";
import TabLayout from "@/src/components/tab-layout";
const TabbedInterface: React.FC = () => {
  const tabs = [
    { value: "tab1", label: "Tab 1" },
    { value: "tab2", label: "Tab 2" },
    { value: "tab3", label: "Tab 3" },
  ];
  const tabContent = {
    tab1: <div>Content for Tab 1</div>,
    tab2: <div>Content for Tab 2</div>,
    tab3: <div>Content for Tab 3</div>,
  };
  const [activeTab, setActiveTab] = useState("tab1");
  useEffect(() => {
    const hash = window.location.hash.replace("#", "");
    if (tabs.some((tab) => tab.value === hash)) {
      setActiveTab(hash);
    }
  }, []);
  const handleTabChange = (tabValue: string) => {
    setActiveTab(tabValue);
    window.history.replaceState(null, "", `#${tabValue}`);
  };
  return (
    <div className="space-y-5 h-full">
      <TabLayout
        tabs={tabs}
        tabContent={tabContent}
        activeTab={activeTab}
        onTabChange={handleTabChange}
      />
    </div>
  );
};
export default TabbedInterface;

By addressing hydration errors, you ensure your Next.js app runs smoothly and delivers an excellent user experience. Understanding the interplay between SSR and hydration is key to fixing these tricky issues. 🚀

More from this blog

N

NonStop io Technologies

164 posts

Founded in August 2015, we are a USA-based Bespoke Engineering Studio providing Product Development as an Expertise.

Understanding Hydration in Next.js & Fixing Hydration Errors