React · intermediate
React hydration mismatch: compare server and browser inputs
A React hydration mismatch occurs when the HTML produced on the server does not match what the client renders during hydration. This guide explains how to identify where the divergence originates, separate environment-dependent causes from code-level causes, and verify the fix by observing that React no longer reports hydration warnings or recovers from the mismatch.
The symptoms
- •React prints a development-mode warning containing the words "Hydration failed" or "Text content does not match server-rendered HTML" in the browser console.
- •The browser shows a flash of corrected content: the page renders, then visibly changes after hydration completes (for example, a date, locale string, or user-specific value updates).
- •Interactive elements stop responding to events until the page is reloaded, even though the markup is present in the DOM.
- •React 18+ falls back to client-side rendering for the affected subtree, which can be inferred from repeated re-renders or missing server-rendered markup in the initial HTML response.
- •Tests using a DOM environment without matching the server pass locally but fail under server-side rendering because the rendered tree diverges.
Likely causes
- •Time- or locale-dependent rendering: code that calls Date.now(), toLocaleString(), Intl.DateTimeFormat, or similar APIs at render time produces different output on server and client because the clock or time zone differs.
- •Direct DOM access during render: reading window, document, navigator, localStorage, matchMedia, or location outside of an effect produces values that exist on the client but not on the server, leading to divergent markup.
- •Conditional rendering based on authentication or feature flags whose value is only known on the client, so the server emits a placeholder and the client replaces it.
- •Non-deterministic ordering of keys, maps, or sets serialized differently across runs (for example, object key enumeration order changes between server and client runtimes).
- •HTML structural errors such as invalid nesting (for example, a div inside a p, or a tr outside a table) that the browser silently rewrites, causing the DOM tree to differ from the server string.
- •Third-party scripts or browser extensions that mutate the DOM between the server response arriving and React hydrating it, so React's expected tree no longer matches.
First ten minutes
- 01Reproduce the warning reliably: load the affected route in a clean browser profile with extensions disabled, then in another profile with extensions enabled, to test the extension-mutation hypothesis early.
- 02Capture the exact warning text and the component stack React prints; the stack usually points to the component whose render output differs.
- 03Diff the initial server response HTML against the DOM after hydration completes, isolating the specific nodes that changed.
- 04Classify each differing node as either "value differs" (text content, attribute, or conditional branch) or "structure differs" (different element, missing wrapper, or reparented child), which points to different causes.
- 05Disable browser extensions and ad/tracker blockers to rule out third-party DOM mutation before changing application code.
- 06Toggle between production and development builds of React; some hydration warnings are emitted only in development, and production may silently re-render the subtree instead.
Evidence to collect
- •The full React hydration warning text from the browser console, including the component stack and the offending server versus client string when React prints one.
- •The raw server response body for the affected URL, saved before any client script runs, compared node-by-node against the post-hydration DOM.
- •The set of environment variables, feature flags, and authentication state visible on the server at request time versus on the client at hydration time.
- •The locale, time zone, and clock values available on both runtimes during the request, to test the time- or locale-dependence hypothesis.
- •The list of scripts that execute between the server HTML arriving and the React hydration call, including third-party tags and any synchronous inline scripts.
- •A minimal reproduction that strips styling, data fetching, and providers to isolate the component whose render output diverges.
Where to look
- •At the render boundary of any component that reads browser-only globals (window, document, navigator, localStorage, matchMedia) during its render function rather than inside useEffect or an event handler.
- •At the boundary where server-rendered props meet client props: serialization of dates, big numbers, maps, sets, or class instances that the JSON serializer does not round-trip identically.
- •At the HTML structure produced by the server, especially inside elements with strict content models (table, select, ul, ol, p, a) where invalid children are quietly rewritten by the browser parser.
- •At the spot in the document where injected third-party scripts (analytics, consent banners, translation overlays) mutate the DOM before React's hydration begins.
- •At the boundary between SSR data and client-only state: components that branch on a flag, a cookie value, or a user role known only after hydration.
Diagnostic steps
- 01Reproduce in a controlled environment: serve the production build, disable all browser extensions, and clear service workers, then confirm whether the warning still appears.
- 02Save the raw server HTML and the post-hydration DOM, then walk the trees to find the first node where they diverge; record the element path, tag name, and differing attribute or text.
- 03For "value differs" nodes, inspect the render code path for time, locale, random, or environment-dependent inputs; verify by hard-coding the input and observing whether the warning disappears.
- 04For "structure differs" nodes, inspect the HTML for parser-strict violations (for example, block elements inside inline contexts, missing tbody around tr, or button inside button); fix the markup so it parses identically on both sides.
- 05Audit every render path for direct reads of window, document, navigator, localStorage, or location; each is a candidate unless it is guarded to only run after mount.
- 06Defer or guard any client-only branch with a "mounted" state set inside useEffect, and verify that the server emits a stable placeholder that matches the first client render.
- 07Move unstable keys out of render: replace object-identity keys or array indices that depend on insertion order with stable identifiers from the data source.
- 08Confirm by reloading the route in a clean browser: no hydration warning in the console, no visible flash of corrected content, and the DOM matches the server HTML before any user interaction.
Common mistakes
- •Suppressing the warning with suppressHydrationWarning on a parent rather than finding the actual divergence; this hides the symptom while the underlying render still mismatches and can break event delegation or accessibility.
- •Reading window or localStorage during render and "fixing" the mismatch by adding suppressHydrationWarning instead of moving the read into useEffect or a server-side data source.
- •Assuming the mismatch is a server bug when the DOM was mutated by a browser extension, an ad blocker, or an inline analytics script that ran before hydration.
- •Rendering different markup for authenticated versus anonymous users without sending the auth state in the initial server response, which guarantees a client-only replacement after hydration.
- •Serializing Date, Map, Set, or BigInt values through JSON without a deterministic replacer, producing different strings on server and client even though the underlying value is the same.
Safe fixes
- •If the divergence is time- or locale-dependent, render a stable placeholder on the server and replace it after mount via useEffect, so both runtimes agree on the initial markup.
- •If a render reads window, document, navigator, localStorage, matchMedia, or location, move that read into a useEffect and store the result in state that starts with a server-safe default.
- •If the divergence is structural, correct the HTML so it is valid in the destination context (for example, move block elements out of inline parents, wrap tr in tbody, replace p with div for non-phrasing content).
- •If authentication or feature flags differ between server and client, include the resolved values in the initial server payload so the first client render matches the server output.
- •If third-party scripts mutate the DOM before hydration, defer their initialization until after React has mounted, or place them in a portal outside the hydrated subtree.
- •If keys depend on non-deterministic ordering, derive them from a stable property of the data (an id from the source) rather than from insertion order or object identity.
Prove the fix
- 01Reload the affected route in a clean browser profile with extensions disabled; the React hydration warning must not appear in the console.
- 02Compare the raw server response HTML against the DOM immediately after hydration completes (before any user interaction or effect runs); the two trees must be identical.
- 03Observe the page visually: there must be no flash of corrected content, no reflow of text or layout after the initial paint, and interactive elements must respond to the first click without a reload.
- 04Run the route through the SSR test harness and assert that the server-rendered string equals the client render output for the same props, locale, and time zone.
- 05Enable React's development build and confirm that no "Hydration failed", "Text content does not match", or "Expected server HTML to contain" warning is logged for the route.
- 06Verify that the component does not silently fall back to client rendering by checking that the server response contains the expected markup and that React does not re-render the subtree on mount.
Prevention and next steps
- •Establish a coding rule that any access to window, document, navigator, localStorage, matchMedia, or location happens inside useEffect or an event handler, never during render.
- •Render only server-deterministic values in the initial markup: pass locale, time zone, user identity, and feature flags in the initial server payload rather than resolving them on the client.
- •Add an SSR equality test for each route that asserts the server-rendered string equals the client render output under fixed props, locale, and clock, and run it in CI.
- •Lint or code-review for HTML produced inside elements with strict content models (table, select, ul, ol, p, a) so invalid nesting is caught before it reaches the browser parser.
- •Document an extension- and blocker-aware test mode that reproduces hydration warnings against a clean browser profile, so third-party DOM mutation is not mistaken for an application bug.
Safe commands and checks
node --version npm ls react react-dom grep -RIn "window\." src --include="*.tsx" --include="*.jsx" grep -RIn "Date\.now\|toLocaleString\|Intl\." src --include="*.tsx" --include="*.jsx" grep -RIn "suppressHydrationWarning" src --include="*.tsx" --include="*.jsx" grep -RIn "localStorage\|sessionStorage\|navigator\." src --include="*.tsx" --include="*.jsx"