CCelestiallines.com
← Back to blog

How to Fix Hydration Errors in Next.js

By Pawan Singh · Jun 2026

A hydration error means the HTML React generated on the server didn't match what it generated on the client during the first render — React detects the mismatch, warns loudly, and (depending on severity) either patches the DOM or throws, both of which are worse than the error simply not happening.

Cause 1: Rendering Anything That Differs Between Server and Client

// Bug: Date.now() (or Math.random()) produces a different value on
// the server (build/request time) than when the client re-renders it
function Timestamp() {
    return <span>{new Date().toLocaleTimeString()}</span>;
}

Anything genuinely non-deterministic between server and client render — the current time, Math.random(), a randomly generated ID — will produce a mismatch, because the server rendered it once (at request/build time) and the client renders it again (at hydration time), inevitably getting a different value. Fix it by computing the value only after mount, so the server-rendered version and the client's first paint agree (both render nothing/a placeholder), and the real value appears in a second, client-only update:

function Timestamp() {
    const [time, setTime] = useState(null);

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

    return <span>{time ?? '--:--:--'}</span>;
}

Cause 2: Checking window or Other Browser-Only Globals During Render

// Bug: `window` doesn't exist during server rendering at all — this
// throws server-side, or if guarded, produces mismatched output
function ResponsiveWidget() {
    const isMobile = window.innerWidth < 768; // crashes on the server
    return <div>{isMobile ? 'Mobile' : 'Desktop'}</div>;
}

Any browser-only API (window, localStorage, navigator) is unavailable during server rendering by definition — there is no browser on the server. Read it inside useEffect (which only runs client-side, after mount) instead of directly in the render body, and store the result in state.

Cause 3: Browser Extensions Modifying the DOM Before React Hydrates

Some browser extensions (password managers, ad blockers, accessibility tools) inject attributes or elements into the page before React hydrates — React sees a DOM that doesn't match what it rendered server-side, even though nothing in your own code is actually wrong. This is diagnosable by testing the exact same page in an incognito window with no extensions; if the warning disappears, it's environmental, not a bug in the app, and generally isn't worth chasing further.

Cause 4: Invalid HTML Nesting

// Bug: a <div> is not valid inside a <p> per the HTML spec — the
// browser silently "fixes" this by restructuring the DOM differently
// than what React actually rendered, causing a mismatch
<p>
    <div>Some content</div>
</p>

The browser's own HTML parser corrects certain invalid nesting patterns automatically (moving, closing, or dropping elements) — producing a DOM structure that no longer matches what React's virtual DOM expects from its own render output. Fixing the actual HTML structure (not nesting block elements inside inline-only-context elements like <p>) resolves this at the source.

Cause 5: A Third-Party Script or Extension That Injects Content, or Locale/Timezone Differences

Server-rendering with one timezone/locale assumption and the client formatting a date in the visitor's actual local timezone is a common, subtle version of Cause 1 — a date formatted with toLocaleDateString() can legitimately differ between a server (often UTC) and a client (the visitor's local timezone), even for the exact same underlying timestamp. Format dates in a fixed, explicit timezone/locale server-side, or defer locale-sensitive formatting to a client-only effect the same way as the timestamp example above.

The suppressHydrationWarning Escape Hatch

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

This silences the warning for a single element without fixing the underlying mismatch — appropriate only when the mismatch is genuinely expected and harmless (a timestamp that's allowed to differ by a few seconds), never as a way to make an unexplained error message go away without understanding why it's happening.

A Systematic Way to Diagnose It

  1. Read the actual diff React prints in the console — recent React versions show exactly which attribute or text content didn't match, not just "a mismatch occurred somewhere."
  2. Reproduce in an incognito window with no extensions first, to rule out Cause 3 before debugging your own code.
  3. Search the flagged component for anything reading Date, Math.random(), window, or other browser-only/non-deterministic values directly in the render body.
  4. Check for invalid HTML nesting around the flagged element.

Chasing a stubborn Next.js hydration bug? Get in touch — I work as a freelance Next.js developer.

Comments