Hydration Proof

Search documentation

Find a page or section

A tree hydrated but some attributes of the server rendered HTML didn't match

React 19's warning for attributes it will never fix on its own.

"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties" is a React 19 development warning. Hydration worked, but an attribute such as className, style, href or id differs, and React keeps the server's value. Users see the wrong attribute until that prop changes. Make the attribute identical in both renders.

The error

React 19 logs it in development, with a diff of every attribute that differs:

A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. 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
 
  <Sidebar>
    <aside
+     className="sidebar wide"
-     className="sidebar"
    >

There is no production form and no minified code: production builds of React 19 do not compare attributes at all.

In React 18 the same problem produced one warning per attribute, such as Prop className did not match and Extra attributes from the server.

Why a tree hydrated but some attributes of the server rendered HTML didn't match

For attributes, React does not throw the page away as it does for text and elements. It attaches to the server's DOM and leaves the attribute as the server wrote it: that is what "this won't be patched up" means.

Later renders do not repair it either. React compares each new render with its own previous props, not with the DOM, so the DOM keeps the server's value until the prop itself changes. A className computed from the screen width stays wrong for as long as the width stays the same.

Read the diff like this:

  • + and - on the same attribute: the value differs.
  • A - line alone: the attribute is only in the server HTML. Often a script or a browser extension added it before React loaded.
  • A + line alone: the browser render has an attribute the server did not send.

Production is silent

React 19 checks attributes only in development. The same mismatch in production logs nothing, and your users keep the wrong value. Test the production build with a tool that compares attributes itself (see below).

Common causes

AttributeUsual sourceFix guide
class or style on <html>A theme script sets dark mode before React loadsTheme
className on componentsCSS-in-JS class names generated in a different orderCSS-in-JS
className from the screen sizewindow.innerWidth or matchMedia read during renderMedia queries
id, htmlFor, aria-*Ids from Math.random() or a counterUnstable ids
datetime, title, aria-labelA formatted time or dateTime, locale
Attributes on <body> nobody renderedA browser extensionBrowser extensions

How to fix it

  1. Read the attribute name and both values in the diff, and the component above it.

  2. Compute the attribute from data both sides have. Before, the class depends on the window, which the server does not have:

    components/sidebar.tsx
    "use client";
     
    export function Sidebar() {
      const wide = typeof window !== "undefined" && window.innerWidth > 1024;
      return <aside className={wide ? "sidebar wide" : "sidebar"} />;
    }

    After, both renders use the default and an effect updates it. Because this is a state change, React applies it to the DOM:

    components/sidebar.tsx
    "use client";
     
    import { useEffect, useState } from "react";
     
    export function Sidebar() {
      const [wide, setWide] = useState(false);
     
      useEffect(() => {
        const update = () => setWide(window.innerWidth > 1024);
        update();
        window.addEventListener("resize", update);
        return () => window.removeEventListener("resize", update);
      }, []);
     
      return <aside className={wide ? "sidebar wide" : "sidebar"} />;
    }

    For layout alone, a CSS media query avoids the second render entirely.

  3. For ids, use useId(). It produces the same id on the server and in the browser.

  4. For attributes a pre-hydration script sets on purpose (a theme class on <html>), add suppressHydrationWarning to that one element. It covers the element's own attributes only. See suppressHydrationWarning.

Find every instance

hydration-proof compares every attribute in the page with the props React rendered, in development and in production, where React itself stays silent:

npx hydration-proof test --mode both

Differences are reported as HP1002 (an attribute), HP1003 (inline style), HP1004 (class), HP1005 (only in the server HTML) and HP1006 (missing from it).

In the editor: no-browser-global-in-render, no-match-media-in-render, no-unstable-id and no-client-only-initial-state report the code that produces these attributes.