Home/Blog/Error: Hydration failed because the server rendered HTML didn't match the client
šŸ’§
reactnextjshydration error

Error: Hydration failed because the server rendered HTML didn't match the client

React's hydration mismatch error explained — the six most common root causes (Date formatting, window/localStorage access, browser extensions, invalid HTML nesting) and the exact fix for each one.

October 16, 20266 min readby ToolNinja

The Exact Error

Error: Hydration failed because the server rendered HTML didn't match the client.

Or, in slightly different phrasing depending on the React version:

Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "..." Client: "..."
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

Quick summary: React rendered your component tree once on the server and sent HTML to the browser, then rendered it again on the client to attach event listeners ("hydration"). The two renders produced different output. React detects the mismatch, logs a warning (and in newer versions, the exact mismatched text or attribute), and re-renders the affected subtree on the client to recover.


Why This Error Happens

Hydration assumes the server render and the first client render produce identical output. Anything that can differ between those two environments breaks that assumption. Six causes account for almost every real-world case:

1. Browser-only APIs read during render — window, document, localStorage, or navigator don't exist on the server. Reading them directly in a component body (not inside useEffect) either throws server-side or silently returns a different value than the client gets.

2. Date and time formatting — new Date().toLocaleString() or toLocaleDateString() without an explicit locale and time zone renders using the server's time zone (usually UTC) and the client's time zone (the visitor's local time) — two different strings for the same instant.

3. Math.random() or crypto.randomUUID() during render — generates a new value on the server and a different one on the client, every single time.

4. Browser extensions modifying the DOM — Grammarly, password managers, and some ad blockers inject attributes or wrapper elements into the page before React hydrates. React sees DOM that doesn't match what it rendered server-side, even though your component code is correct.

5. Invalid HTML nesting — a <div> inside a <p>, or a <tr> not inside a <table>/<tbody>, gets silently corrected by the browser's HTML parser into a different DOM structure than React actually rendered, which hydration then detects as a mismatch.

6. typeof window !== "undefined" branches in render — a common pattern for "only run this on the client," but if it's used directly in the render body rather than inside useEffect, the server (where window is undefined) and client (where it's defined) render two different trees on purpose.


Step-by-Step Diagnosis

Step 1 — Read the diff React gives you

In development mode, React prints the actual mismatched text or attribute:

Warning: Text content did not match. Server: "11/4/2026" Client: "4/11/2026"

That's almost always enough to identify the cause directly — here, a date formatted with two different locales.

Step 2 — Rule out a browser extension

Open the page in an incognito/private window with extensions disabled. If the warning disappears, it's an extension injecting markup, not your code — safe to ignore (or add suppressHydrationWarning to the specific element it's modifying, usually <body>).

Step 3 — Search your component for render-time browser API access

function Clock() {
  const now = new Date(); // āŒ different on server vs. client
  return <span>{now.toLocaleTimeString()}</span>;
}

Any window, localStorage, Date.now(), or Math.random() called directly in the function body (not inside useEffect) is a candidate.


Solutions

Solution 1 — Move browser-only reads into useEffect

function Clock() {
  const [time, setTime] = useState<string | null>(null);

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

  return <span>{time ?? "—"}</span>; // same placeholder on server and first client render
}

The server renders the placeholder. useEffect only runs on the client, after hydration completes — so the first client render still matches the server exactly, and the real value appears a tick later.

Solution 2 — Format dates with an explicit, fixed locale and time zone

// āŒ Renders differently depending on server/client time zone
new Date(timestamp).toLocaleDateString();

// āœ… Same output everywhere, until you intentionally localize client-side
new Intl.DateTimeFormat("en-US", { timeZone: "UTC" }).format(new Date(timestamp));

If you want the visitor's local time zone, render the UTC version first (matching the server), then swap to local time inside useEffect — the same pattern as Solution 1.

Solution 3 — Skip SSR entirely for a genuinely client-only component

import dynamic from "next/dynamic";

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

For a component that fundamentally can't render the same way on the server (a canvas widget, a browser-API-dependent chart), this is simpler than fighting hydration — it just doesn't render on the server at all, so there's nothing to mismatch.

Solution 4 — Fix invalid HTML nesting

// āŒ <div> isn't valid inside <p> — browser auto-corrects, React doesn't expect that
<p><div>Note</div></p>

// āœ…
<div><div>Note</div></div>

Solution 5 — suppressHydrationWarning for known, intentional differences

<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>

Use this only when the mismatch is expected and harmless (a live clock, a "time ago" label) — it silences the warning for that element's text/attributes specifically, not the whole subtree, and it doesn't fix anything underneath it.


Quick Reference

CauseFix
window/localStorage read during renderMove into useEffect, render a placeholder first
Locale-dependent date formattingFixed Intl.DateTimeFormat locale + time zone
Math.random() / crypto.randomUUID() in renderGenerate once, pass down as a prop, or move to useEffect
Browser extension modifying the DOMNot a code bug — verify in incognito, ignore or suppressHydrationWarning on <body>
Invalid HTML nestingFix the markup structure
Genuinely client-only componentnext/dynamic(..., { ssr: false })

Prevent This Error in the Future

1. Never call Date.now(), Math.random(), or read window/localStorage directly in a component's render body. If a value needs to differ between server and client, compute it in useEffect and start with a stable placeholder.

2. Always pass an explicit locale and time zone to date formatting functions rather than relying on the runtime's default, which differs between your server and every visitor's browser.

3. Validate HTML nesting — most hydration mismatches from invalid markup show up immediately in development; don't ignore nesting warnings from linters or React DevTools.


Use ToolNinja to Debug Faster

When React's dev-mode warning shows you a server/client text diff but it's buried in a long component tree, pasting the two strings into a diff tool makes the actual difference (a stray space, a different date format, a locale mismatch) immediately visible.

šŸ”§ Diff Checker — toolninja.io/tools/diff-checker

Share:š• Twitterin LinkedIn

Frequently Asked Questions

Is a hydration mismatch always a bug in my code?

No. React 18+ recovers automatically from most mismatches by discarding the server-rendered DOM for that subtree and re-rendering it on the client — the page still works, but the console warning is real and worth fixing. A common non-bug cause is a browser extension (Grammarly, password managers, ad blockers) injecting attributes or elements into the DOM before React hydrates. Test in an incognito window with extensions disabled before assuming it's your code.

What does suppressHydrationWarning actually do?

It silences the mismatch warning for one specific element's text content or attribute — it does not fix the underlying mismatch, and React still uses the client-rendered value after hydration. It's appropriate for values you know will legitimately differ (like a formatted "last updated" timestamp), not as a way to hide an actual bug.

Why does this only happen in production, not in dev?

It usually does happen in dev too, but development builds show a more detailed diff of exactly which DOM node and attribute mismatched, so it's easier to miss as 'just a warning.' Production builds show the shorter, more cryptic version of the same message. If you're only seeing it in production, double-check that dev mode isn't somehow bypassing the code path — a server/client branch gated behind an environment check is a common reason.

Does getServerSideProps or generateStaticParams data cause hydration errors?

Only indirectly — if the data itself is consistent between server and client, it won't cause a mismatch. The actual trigger is almost always something computed *during render* that differs between the two environments (the current time, a random value, a browser-only API), not the data-fetching step itself.

🄷 ToolNinja