The common causes of hydration errors all break one rule: the first render in the browser must produce exactly the HTML the server sent. Clocks, random values, locales, time zones, browser-only APIs, storage, themes, different data and invalid nesting each break it in their own way. Every cause below has a fix guide.
All common causes of hydration errors
hydration-proof attaches one of these cause ids to each finding, and links the finding to the page for that id:
| Cause id | What it means |
|---|---|
| time | Time-dependent value |
| timezone | Timezone difference |
| locale | Locale-dependent formatting |
| random | Random value |
| browser-api | Browser-only API used during render |
| storage | localStorage or sessionStorage read during render |
| media-query | Screen size or media query read during render |
| theme | Theme preference (dark or light mode) |
| data | Server and client used different data |
| invalid-html | Invalid HTML nesting |
| css-in-js | CSS-in-JS class names differ |
| extension | Browser extension |
| third-party-script | A script changed the page before hydration |
| cdn | HTML rewritten by a CDN or proxy |
| unstable-id | Generated id differs |
| form-state | Server Action form state |
| suppressed | Intentional difference (suppressHydrationWarning) |
What React lists as the causes
React 19 prints its own short list with every hydration error. Each line maps to one or more of the pages above:
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.| React's line | Cause pages |
|---|---|
| A server/client branch | Browser APIs, storage, media queries, theme |
Date.now() or Math.random() | Time, random values, generated ids |
| Date formatting in a user's locale | Locale, timezone |
| External changing data | Data, form state |
| Invalid HTML tag nesting | Invalid HTML |
| A browser extension | Extensions, third-party scripts, CDNs |
The list leaves out CSS-in-JS class names, which
differ when a style library generates them in a different order on each side,
and intentional differences hidden with
suppressHydrationWarning.
How hydration-proof picks a cause
The likely cause is an informed guess built from four kinds of evidence. The strongest signal wins, and every other signal that points at the same cause adds a little confidence:
- The two values. Millisecond timestamps, the same number with different
separators, random decimals and UUIDs, class names that look generated
(
sc-,css-,emotion-), theme classes (dark,light), React ids and whitespace-only differences each point at one cause. - The code near the line. hydration-proof maps the element to your source
file and scans the component that rendered it for
Date.now(),localStorage,matchMedia,fetch,typeof windowand similar calls. Code in a neighboring component never explains a finding. - The scenario. A browser timezone or locale that differs from the server's, dark mode, a mobile viewport or pre-filled storage.
- The stage. Markup the browser's parser repaired is invalid nesting. A
change between parsing and hydration came from a script or an extension. A
form the server rendered with Server Action state is form state. A
difference on an element with
suppressHydrationWarningis intentional.
The report shows the cause with a confidence score, for example
timezone difference, 95%, and the reason next to it. Below 35%, no cause is
named. Some causes cannot be read off the values at all: a typeof window
branch only shows in the code, so run in development mode for exact source
lines. How it works covers the six snapshots behind each
comparison.
Prove the cause with probes
--probe turns the guess into proof. hydration-proof loads the page again with
the browser clock and random values fixed, then once for each factor with
exactly one thing changed:
| Factor | What changes |
|---|---|
time | The browser clock moves by 3 days, 7 hours, 11 minutes and 13 seconds |
random | A different seed for Math.random() and crypto.getRandomValues() |
locale | The server's locale, or another one |
timezone | The server's timezone, or another one |
theme | Light and dark swap |
viewport | Desktop and mobile swap |
storage | The page loads without the scenario's localStorage, sessionStorage and storageState |
If the client value changes, or the finding disappears, when only one factor
changes, that factor is the proven cause, and the report says proven instead
of a percentage. A page that renders differently on an
identical reload depends on server data. The server is never changed, so a
fixed browser clock cannot hide a time-dependent value. See
probes for the options.
npx hydration-proof test --probeCauses, ESLint rules and issue codes
The ESLint plugin catches most causes while you type. hydration-proof catches all of them in the running app, including the ones no linter can see. Confidence is what the package's own fixture suite measured for a production build.
| Cause | Typical issue codes | ESLint rules | Measured confidence |
|---|---|---|---|
| time | HP1001, HP1002 | no-date-in-render | 97% |
| timezone | HP1001 | no-timezone-without-explicit-timezone | 95% |
| locale | HP1001 | no-locale-without-explicit-locale | 98% |
| random | HP1001, HP1002 | no-random-in-render | 94% |
| browser-api | HP1001, HP1002, HP1003 | no-browser-global-in-render, no-window-render-branch, no-client-only-initial-state | 57–59% |
| storage | HP1001 | no-storage-in-initial-render | 94% |
| media-query | HP1001, HP1004 | no-match-media-in-render | 89% |
| theme | HP1004, HP1002 | audit-suppress-hydration-warning | 99% |
| data | HP1001, HP1007–HP1009 | none | 68% |
| invalid-html | HP3001, HP3002 | no-invalid-interactive-nesting | 97% |
| css-in-js | HP1004 | none | 90% |
| extension | HP4002, HP4001 | none | not measured |
| third-party-script | HP4001 | none | 80% |
| cdn | HP1015, HP4003 | none | 88% |
| unstable-id | HP1002, HP3003, HP3004 | no-unstable-id, no-global-render-counter | not measured |
| form-state | HP1012 | none | not measured |
| suppressed | HP6001–HP6003 | audit-suppress-hydration-warning | not measured |
browser-api scores lower because the values alone never show it: the cause
comes from the code, which is exact in development builds and depends on
source maps in production.