"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
| Attribute | Usual source | Fix guide |
|---|---|---|
class or style on <html> | A theme script sets dark mode before React loads | Theme |
className on components | CSS-in-JS class names generated in a different order | CSS-in-JS |
className from the screen size | window.innerWidth or matchMedia read during render | Media queries |
id, htmlFor, aria-* | Ids from Math.random() or a counter | Unstable ids |
datetime, title, aria-label | A formatted time or date | Time, locale |
Attributes on <body> nobody rendered | A browser extension | Browser extensions |
How to fix it
-
Read the attribute name and both values in the diff, and the component above it.
-
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.
-
For ids, use
useId(). It produces the same id on the server and in the browser. -
For attributes a pre-hydration script sets on purpose (a theme class on
<html>), addsuppressHydrationWarningto 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 bothDifferences 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.