Hydration Proof

Search documentation

Find a page or section

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

React 19's hydration error, and how to read the diff under it.

"Hydration failed because the server rendered HTML didn't match the client" is the error React 19 throws when the first render in the browser produces different elements or text than the HTML the server sent. React throws that HTML away and renders the tree again on the client. The diff under the message shows what to fix.

The error

In development, React 19 prints the message, the causes it suspects, a link and a diff of the first difference it found:

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
 
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
 
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
 
https://react.dev/link/hydration-mismatch
 
  <Clock>
    <p>
+     12:04:33
-     12:04:31

Newer React 19 releases say "text" instead of "HTML" when only text differs:

Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.

A production build prints only a code, Minified React error #418, with no diff. Minified React error #418 explains how to get the full message back.

Why hydration failed because the server rendered HTML didn't match the client

Hydration reuses the server's HTML instead of building the page again. React renders your components in the browser once, walks the existing DOM alongside that render, and attaches event handlers to the nodes it finds. That only works when the two agree node for node.

When an element or a text node differs, React stops. "This tree will be regenerated" means React discards the server HTML up to the nearest <Suspense> boundary, or the whole page if there is none, and renders that part again in the browser. The page still works, but:

  • the server's work is wasted and the page renders twice,
  • the content can flash while it is replaced,
  • text a user typed and the focused element are lost.

This is the React 19 hydration error for elements and text. A React 19 hydration mismatch in an attribute (className, style, href) is different: React keeps the page and only warns, with "A tree hydrated but some attributes… didn't match".

The Next.js 15 "Hydration failed because the server rendered HTML didn't match the client" overlay shows the same React message and diff: Next.js 15 runs the App Router on React 19.

How to read the diff

The diff is written like a code review, from the client's point of view:

  • Lines starting with + are what the browser rendered.
  • Lines starting with - are what the server HTML contained.
  • Unmarked lines are the path from the component down to the difference.

A + and - pair on the same element is a changed value. A + line alone is an element the server never sent, and a - line alone is one the browser did not render. React stops at the first difference, so fix it and reload: there may be more.

Common causes

React's own list covers most cases. Each row links to the fix guide:

CauseTypical diffFix guide
A typeof window check, window or navigator read during render+ <canvas> against - <div>Browser-only APIs
localStorage, theme or screen size read during renderA different element or classStorage, theme, media queries
Date.now(), new Date() or relative times+ 12:04:33 against - 12:04:31Time-dependent values
Math.random() or countersTwo different ids or keysRandom values, unstable ids
Dates and numbers formatted in the user's locale or timezone+ 18/09/2026 against - 9/18/2026Locale, timezone
Data fetched again in the browserTwo different prices or listsServer and client data
Invalid HTML nesting, such as <div> in <p>An element in an unexpected parentInvalid HTML
A browser extension, a third-party script or a CDN that edits the HTMLNodes nobody renderedExtensions, scripts, CDNs

How to fix it

  1. Open the page in development and read the diff. Find the first + and - pair and the component name above it.

  2. Find where the value comes from. Match it against the table above: a time, a locale-formatted number, a value from window or storage.

  3. Make the first client render produce the server's output. Either compute the value on the server and pass it down, or render a neutral value first and switch to the browser value after hydration:

    components/greeting.tsx
    "use client";
     
    // Before: the server renders "guest", the browser renders the stored name.
    export function Greeting() {
      const name =
        typeof window !== "undefined" ? localStorage.getItem("name") : null;
      return <p>Hello, {name ?? "guest"}</p>;
    }
    components/greeting.tsx
    "use client";
     
    import { useEffect, useState } from "react";
     
    // After: both renders say "guest", then the effect loads the stored name.
    export function Greeting() {
      const [name, setName] = useState<string | null>(null);
     
      useEffect(() => {
        setName(localStorage.getItem("name"));
      }, []);
     
      return <p>Hello, {name ?? "guest"}</p>;
    }

    useEffect and two-pass rendering explains the pattern. For a component that cannot render on the server at all, use a client-only component.

  4. Reload and check the console again. React reports one difference per boundary, so the next one appears once the first is fixed.

  5. Wrap slow or risky parts in <Suspense>. It does not fix the mismatch, but React then re-renders only that boundary instead of the page.

suppressHydrationWarning hides a text or attribute difference on one element, one level deep. It does not help when elements differ. See when suppressHydrationWarning is safe.

Find every instance

The console shows one mismatch per load, in the page you have open. hydration-proof loads every route in a real browser and compares the server HTML with the hydrated DOM:

npx hydration-proof test --mode both

It reports the exact difference as HP1001 (text), HP1007 (a different element), HP1008 (an element only in the server HTML) or HP1009 (an element missing from it), and what React did about it as HP1010 (one branch rendered again) or HP1011 (the whole page). When React reports the error but the DOM comparison cannot place it, you get HP2001. Each finding has the source line and the likely cause.

In the editor, the ESLint plugin catches the code patterns React lists: no-window-render-branch, no-date-in-render, no-random-in-render, no-locale-without-explicit-locale and no-invalid-interactive-nesting.