DEV Community

Rutuja
Rutuja

Posted on

Fix Hydration Errors in Next.js

๐Ÿ” 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;
Enter fullscreen mode Exit fullscreen mode

โ“ 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);
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› ๏ธ 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}
/>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› ๏ธ 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;

Enter fullscreen mode Exit fullscreen mode

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. ๐Ÿš€

Top comments (0)