CalcSnippets Search
Web 3 min read

Next.js Hydration Errors Explained With Real Fixes, Not Hand-Wavy Advice

A direct guide to diagnosing and fixing Next.js hydration mismatches, including browser-only APIs, time-dependent rendering, invalid HTML structure, and client-only components.

Why this error is so annoying: hydration failures make frontend teams doubt everything at once. Is the problem React? Is it SSR? Is it a race condition? Sometimes the error message is vague, but the root causes are usually very ordinary.

What hydration mismatch actually means

When Next.js server-renders a page, the HTML sent from the server has to match what React expects when the client takes over in the browser. If the server output and client output differ at the moment React hydrates, you get the classic mismatch errors.

The official Next.js docs point to several common causes, and they are worth taking seriously because most real bugs fall into this list:

  • invalid HTML nesting
  • browser-only APIs used during render
  • time-dependent values like Date()
  • random values like Math.random()
  • client/server branches that produce different markup

Case 1: browser-only APIs inside render

This is one of the fastest ways to break hydration.

Bad:

export default function ThemeLabel() {
  const theme = localStorage.getItem("theme");
  return <span>{theme}</span>;
}

Why it fails:

  • the server cannot read localStorage
  • the client can
  • server markup and client markup diverge

Fix it by moving browser-only logic into an effect:

"use client";

import { useEffect, useState } from "react";

export default function ThemeLabel() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const saved = window.localStorage.getItem("theme");
    if (saved) setTheme(saved);
  }, []);

  return <span>{theme}</span>;
}

Case 2: time-dependent rendering

This pattern quietly causes mismatches:

export default function Timestamp() {
  return <div>{new Date().toISOString()}</div>;
}

The server time and client time are not guaranteed to match.

Safer pattern:

"use client";

import { useEffect, useState } from "react";

export default function Timestamp() {
  const [time, setTime] = useState("");

  useEffect(() => {
    setTime(new Date().toISOString());
  }, []);

  return <div>{time || "Loading..."}</div>;
}

If you deliberately want client and server to differ for a specific text fragment, Next.js also documents suppressHydrationWarning, but that should be a narrow escape hatch, not your default design.

Case 3: invalid HTML structure

This one feels embarrassing because the app may “look fine” while still breaking hydration.

Bad:

export default function BrokenMarkup() {
  return (
    <p>
      Intro text
      <div>Nested block</div>
    </p>
  );
}

That is invalid structure. Browsers repair invalid HTML differently, which means the DOM React hydrates may not match the one it expected.

Correct it:

export default function ValidMarkup() {
  return (
    <div>
      <p>Intro text</p>
      <div>Nested block</div>
    </div>
  );
}

Case 4: client-only libraries that should not SSR

Charts, editor widgets, maps, and some UI libraries are often better loaded client-side only.

Use dynamic import with SSR disabled:

import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  ssr: false,
});

export default function Dashboard() {
  return <HeavyChart />;
}

This is better than pretending the library is SSR-safe when it is not.

A debugging workflow that works fast

When you hit hydration issues, do not change five files at once. Use a narrower checklist:

  1. Find the smallest component mentioned in the stack trace
  2. Look for window, document, localStorage, sessionStorage
  3. Look for Date(), Math.random(), locale formatting, or unstable IDs
  4. Look for invalid nesting
  5. Check whether a third-party component should be client-only

The bigger architectural lesson

A lot of hydration bugs are really rendering-discipline bugs. The server render path should stay predictable. The client enhancement path can be dynamic, but only after hydration. Teams that mix those two phases carelessly tend to keep rediscovering the same bug under different names.

If you want fewer hydration errors, the rule is simple: render stable markup first, then layer on browser-only behavior intentionally.

Sources

Keep reading

Related guides