Hydration Proof

Search documentation

Find a page or section

Common causes of hydration errors

One fix guide per root cause, linked from every finding.

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 idWhat it means
timeTime-dependent value
timezoneTimezone difference
localeLocale-dependent formatting
randomRandom value
browser-apiBrowser-only API used during render
storagelocalStorage or sessionStorage read during render
media-queryScreen size or media query read during render
themeTheme preference (dark or light mode)
dataServer and client used different data
invalid-htmlInvalid HTML nesting
css-in-jsCSS-in-JS class names differ
extensionBrowser extension
third-party-scriptA script changed the page before hydration
cdnHTML rewritten by a CDN or proxy
unstable-idGenerated id differs
form-stateServer Action form state
suppressedIntentional 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 lineCause pages
A server/client branchBrowser APIs, storage, media queries, theme
Date.now() or Math.random()Time, random values, generated ids
Date formatting in a user's localeLocale, timezone
External changing dataData, form state
Invalid HTML tag nestingInvalid HTML
A browser extensionExtensions, 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:

  1. 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.
  2. 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 window and similar calls. Code in a neighboring component never explains a finding.
  3. The scenario. A browser timezone or locale that differs from the server's, dark mode, a mobile viewport or pre-filled storage.
  4. 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 suppressHydrationWarning is 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:

FactorWhat changes
timeThe browser clock moves by 3 days, 7 hours, 11 minutes and 13 seconds
randomA different seed for Math.random() and crypto.getRandomValues()
localeThe server's locale, or another one
timezoneThe server's timezone, or another one
themeLight and dark swap
viewportDesktop and mobile swap
storageThe 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 --probe

Causes, 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.

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.