Hydration failed: UI does not match server-rendered HTML (React / Next.js)

React expected the browser DOM to match the HTML the server sent. Something rendered differently on the client — a date, a random id, locale text, or an extension that rewrote the page — and hydration aborted with a mismatch warning.

What hydration is supposed to do

In SSR / RSC frameworks (Next.js, Remix, etc.) the server sends HTML so the first paint is fast. React then hydrates: attach event handlers to that existing markup. If render() on the client would produce different text or structure, React warns — Hydration failed because the initial UI does not match what was rendered on the server or Text content does not match server-rendered HTML — and may rebuild the subtree.

// Classic mismatch: server time ≠ client time
export default function Page() {
  return <p>{new Date().toLocaleString()}</p>;
}

Cause table

PatternWhy it mismatchesFix
Date.now() / new Date() in renderServer and browser clocks / TZ differRender a stable placeholder; set time in useEffect; or pass a server-fixed ISO string
Math.random() / uuid() in renderNew value every render passGenerate ids on the server and pass as props, or create in useEffect / useId
typeof window !== "undefined" branchesServer takes the false branch, client the trueDefault to server-safe UI; enhance after mount
localStorage / window.innerWidthBrowser-only APIs crash or differ on serverRead them in useEffect; never during SSR render
Locale / toLocaleStringServer locale ≠ user localePass explicit locale / timeZone; or format on the client only
Invalid HTML nestingBrowser “fixes” DOM (<p><p>, <a><a>)Valid nesting; React then sees a different tree
Browser extensionsGrammar/password/dark-mode tools inject nodesReproduce in private window; warn users; don’t suppress blindly
Whitespace / HTML entitiesMarkdown or CMS tweaks text nodesDiff View Source vs client; normalize content

Client-only the right way

function ClientTime() {
  const [text, setText] = React.useState("—");
  React.useEffect(() => {
    setText(new Date().toLocaleString());
  }, []);
  return <time>{text}</time>;
}

First paint shows a stable placeholder (same on server and client); the real value appears after mount — no mismatch. For a whole widget, dynamic import with ssr: false (Next.js) is the heavy option.

suppressHydrationWarning — sparingly

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

View Source vs DevTools

Need a stable “now” for APIs? Unix time converter. Mystery injected markup? What is this?

Next.js notes

Related

Invalid hook call (React) · TypeError: Failed to fetch · CORS errors · Invisible zero-width characters · Cannot use import statement outside a module · Text diff · Unix time

Questions

What does Hydration failed because the initial UI does not match mean?

React’s first client render produced different markup than the HTML the server sent, so attaching to that HTML is unsafe.

Why does Date.now() break hydration?

The server stamps one time; the browser renders another. Put time updates in useEffect or pass a fixed server value.

Can a browser extension cause this?

Yes — many inject nodes or rewrite text before/during hydration. Test in a private window with extensions disabled.

Should I always use suppressHydrationWarning?

No. Use it only on small intentional differences (like a clock). Fix impure render logic instead.

How do I debug it quickly?

Compare View Page Source (SSR) with DevTools Elements; search the component for Date, random, window, localStorage, and locale formatters.

Is this a Next.js-only bug?

No — any React SSR/hydration setup can hit it. Next.js just surfaces the warning often because SSR is the default.

Published .

Try it

Related guides

Newest guides