CONCEPT Cited by 2 sources
Hydration mismatch¶
A hydration mismatch is the failure mode where a React (or similar SSR-then-CSR framework) app produces different markup on the server than on the client for the same component, at the same place in the tree, on the initial render. At hydration time the client's React instance walks the server-rendered DOM and compares what it would render to what's already there; on the first divergence it can't safely attach event handlers because the fiber and the DOM node disagree on identity.
React 18's hydrateRoot API is substantially stricter than
React 17's hydrate: where 17 would silently drop to
client-side-rendering and repaint (user-visible but rarely
alarmed), 18 calls onRecoverableError on every mismatch and can
cascade — one mismatch knocks the fiber-list-vs-DOM-list comparison
out of alignment, and subsequent nodes also report errors that are
false-positive consequences of the first.
Why it matters¶
A hydration mismatch has three user-visible consequences:
- The mismatched subtree re-renders client-side — TTVC regresses to approximately client-rendering time for that subtree.
- Interactivity is delayed on the affected subtree because event handlers don't attach until the re-render commits.
- Visual flicker / layout shift if the server-rendered and client-rendered markup differ in size or position.
At scale, a codebase with dozens of mismatch root causes across hundreds of components produces a Sentry-log tidal wave that's hard to act on without dedup (see patterns/first-error-only-hydration-error-reporting).
Taxonomy (Zalando 2023)¶
The Zalando Rendering Engine post (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react) gives the wiki's most detailed production taxonomy, surfaced from "hundreds of Renderers" on the Fashion Store codebase after the React-18 migration. Four categories:
1. Timers / time-deltas¶
Server computes a time-delta (targetDate.getTime() - Date.now())
at SSR time t₀; client re-computes at t₀ + δ. Values differ by
δ — guaranteed mismatch. No application bug — the mismatch is
semantically expected.
Fix: suppressHydrationWarning={true} on the closest wrapping
element. See
patterns/suppress-hydration-warning-for-unavoidable-mismatch.
Caveat: only one level deep; wrap precisely.
2. Timezone-localised dates¶
Intl.DateTimeFormat(locale) or date.toLocaleString(locale)
without an explicit timeZone parameter defaults to the host's
timezone. The SSR server's host timezone (e.g. UTC in a
containerised deployment) and the user's browser host timezone
(whatever Central Europe is currently on) differ, and so do the
rendered dates.
Fix (application-side): always pass an explicit timeZone:
Fix (architecture-side): move date localization to the backend; the component renders a pre-localised string. See patterns/backend-localization-for-hydration-stability.
See concepts/locale-host-default-ssr-csr-divergence for the general form.
3. Number localization (and a Safari de-AT bug)¶
Similar to dates: (12345).toLocaleString() without a locale
uses the host's. Pass an explicit locale to fix most cases.
But one specific case can't be fixed in application code:
"Safari browser where for the de-AT locale, the localisation APIs (like
Intl.NumberFormatortolocalestring) generate values like"2.345"but other browsers including Chrome and Firefox as well as Node.js generate values like"2 345"for the same locale!" (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)
This is a pure runtime-divergence between the Node.js SSR
server and Safari, with neither fixable application-side. The
only fix is
moving
localization to the backend so the rendered string bypasses
both Intl instances.
4. Invalid HTML nesting¶
React 18 treats structurally invalid HTML as a hydration mismatch. Examples:
<div>inside<p>(browser parser auto-closes the<p>before the<div>, so the DOM disagrees with the fiber tree).<button>inside<button>(interactive-content nesting restriction).
The root cause: the HTML parser corrects structurally-invalid markup at parse time, producing a DOM that doesn't match what React's renderer thinks it produced.
Fix: use semantically valid HTML nesting. The post recommends
eslint-plugin-validate-jsx-nesting
for enforcement.
Detection signals (Sentry)¶
Hydration-mismatch logs carry these signals:
componentStackfromhydrateRoot'sonRecoverableError(error, errorInfo)— the React fiber stack at time-of-error. In dev builds, readable; in prod builds, minified — source-map unminification needed.validateDOMNesting(...)substring — indicates invalid-HTML-nesting root cause (category 4).- Multiple errors per single mismatch — because post-first- mismatch the fiber-vs-DOM list alignment breaks and subsequent nodes also fail. The cause fiber is usually not the first logged fiber. See patterns/first-error-only-hydration-error-reporting.
Debugging recipe (Zalando 2023)¶
"In other cases where the cause is not very obvious, what we
found helpful was to check the React dev bundle
(react-dom/umd/react-dom.development.js) and put debuggers on
places which log the hydration errors (usually the
checkForUnmatchedText or throwOnHydrationMismatch functions).
Then by loading the page, try to find out what is the exact React
fiber that causes the issue, and based on that find the
component/element. Don't be afraid to go higher in the stack and
use more debuggers! In some cases we realized that the fiber is
the same element that caused the issue, but in others, it's more
confusing as the fiber is something that was rendered after a
mismatching (usually missing) node instance that was the actual
cause of the issue."
(Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)
Key dev-bundle functions to breakpoint:
checkForUnmatchedText— where text-content mismatches are detected.throwOnHydrationMismatch— the throw path, with the failing fiber in scope.
Variables worth inspecting at the breakpoint: fiber,
nextInstance, current, and their received props.
Interaction with streaming SSR¶
Under concepts/streaming-ssr, hydration runs per Suspense
boundary. A mismatch is contained to the boundary it occurs in;
other boundaries hydrate successfully. But: the state that drove
server rendering of a boundary must be injected before the
boundary's markup or every boundary of a type hits the same
mismatch. Confluence
(sources/2026-04-16-atlassian-streaming-ssr-confluence) uses a
NodeJS objectMode transform that flushes state before markup on
setImmediate.
Interaction with mount-gated rendering¶
For content that genuinely should differ SSR vs CSR (device- specific banners, browser-only state), the escape hatch is mount-gated rendering: render a fallback on SSR and the real content after client mount. This side-steps the mismatch at the cost of a post-hydration render of that subtree.
Seen in¶
- sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react
— canonical wiki instance. Four-category taxonomy with
concrete code pairs, the Safari de-AT
Intl.NumberFormatbug, the "first-error-only" Sentry dedup pattern, and the dev-bundle debugger recipe. Surfaced from a production e-commerce React codebase with "hundreds of Renderers" post-React-18 upgrade. - sources/2026-04-16-atlassian-streaming-ssr-confluence —
Atlassian Confluence's React 18 streaming SSR pipeline; the
state-injection-ordering discipline (state before markup per
boundary) is the streaming-SSR-specific prevention, and the
React 18
useContext-across-ready-boundary bug is a subset of mismatch later fixed in React 19.
Related¶
- concepts/react-hydration — the successful counterpart.
- concepts/concurrent-rendering-react — why React 18 surfaces more of these than React 17.
- concepts/streaming-ssr — where per-boundary hydration changes the blast-radius shape.
- concepts/locale-host-default-ssr-csr-divergence — the general form of categories 2 and 3.
- patterns/suppress-hydration-warning-for-unavoidable-mismatch
- patterns/mount-gated-client-only-rendering
- patterns/backend-localization-for-hydration-stability
- patterns/first-error-only-hydration-error-reporting