frontend

React Hydration Mismatch: Every Cause and Fix (2026)

August 10, 2026

React Hydration Mismatch: Every Cause and Fix (2026)

A React hydration mismatch happens when your server-rendered HTML differs from what React renders on its first client pass. React logs one error with a diff, then discards the server HTML from the nearest Suspense boundary down and re-renders it. Fix the divergence rather than silencing the warning.

TL;DR

React's own documentation is blunt about this: "You should treat mismatches as bugs and fix them."1 Between them, the React and Next.js docs name nine causes — invalid HTML nesting, typeof window branches in render, browser-only APIs read during render, time-dependent APIs, browser extensions, CSS-in-JS misconfiguration, edge proxies that rewrite HTML, stray whitespace around the React root, and simply rendering different data on each side.21 Since React 19 you get a single console error containing a + Client / - Server diff, which usually points straight at the offending element.3

The fix is almost never suppressHydrationWarning. That prop silences one element, one level deep, and React explicitly will not patch that element's mismatched text.1 Everything below is written against react@19.2.8 and next@16.3.0, the releases carrying the latest npm tag as of August 2026.45

What you'll learn

  • What the "Hydration failed because the server rendered HTML didn't match the client" message actually means
  • The full documented cause list, straight from the React and Next.js docs
  • How to find the component responsible using React's diff
  • Why browser extensions still break hydration in React 19, despite the improvements
  • Whether suppressHydrationWarning fixes anything, and when it is the right call
  • How to render client-only content correctly, with useEffect versus useSyncExternalStore
  • How to stop dates, times, and locales from diverging between server and client
  • Why next/dynamic with ssr: false throws in the App Router
  • Why useId values can diverge between server and client
  • Whether a CDN or edge proxy can be the culprit
  • What a hydration mismatch costs you in production

What does "Hydration failed because the server rendered HTML didn't match the client" mean?

It means React attached to server-generated HTML, rendered your component tree once in the browser, and got a different result. Hydration is the step where React "converts the prerendered HTML from the server into a fully interactive application by attaching event handlers."2 When the two renders disagree, React cannot safely attach those handlers to the existing DOM, so it discards the server HTML and re-renders on the client. The blast radius is larger than most people expect. React unwinds to the nearest Suspense boundary above the mismatch and client-renders from there; with no boundary above it, that means discarding the server HTML for the whole root.6 React's own release notes describe the same outcome from the other direction, referring to what happens "if React needs to re-render the entire document due to an unrelated hydration mismatch."3

Before React 19 this surfaced as a pile of separate warnings — Warning: Text content did not match. Server: "Server" Client: "Client", followed by Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>., followed by a thrown error.3 React 19 collapsed all of that into one message:

Uncaught Error: 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
an 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

That bullet list is not generic advice — it is React telling you which five buckets to check first.3

What causes a React hydration mismatch?

Next.js publishes a numbered list of seven causes on its error page for this message. React's hydrateRoot reference publishes a shorter list of four, two of which overlap with Next.js and two of which are additions.21 Take the union and you get nine. Causes 1–7 below are Next.js's list in its own order, causes 8 and 9 are React's additions, and the right-hand column is our own diagnostic shorthand rather than anything either document says:

#Cause (from the docs)Typical tell (ours)
1Invalid HTML nesting — <div> or <ul> inside <p>, <a> inside <a>, <button> inside <button>Diff shows an element the browser silently relocated
2A typeof window !== 'undefined' branch in renderContent appears only after JS loads
3Browser-only APIs like window, localStorage or window.matchMedia read during renderA guarded read returns one value on the server, another in the browser
4Time-dependent APIs such as the Date() constructorTimestamps differ by milliseconds or timezone
5Browser extensions modifying the HTML before React loadsReproduces in your everyday profile, not in a clean one
6Incorrectly configured CSS-in-JS librariesDiff shows two different generated className values
7An Edge/CDN layer that rewrites the HTML responseReproduces through the CDN, not against the origin
8Extra whitespace or newlines around the React root inside the HTML shellDiff points at a text node you never wrote
9Rendering different data on each side — non-deterministic sort, Math.random(), an un-snapshotted external storeDiff shows real content in both columns, just in the wrong order or with the wrong values

Cause 8 is worth a caveat: it applies to hand-written HTML shells, so it is reachable in Vite or a custom SSR setup and largely out of reach in the App Router, where you do not author the shell. Cause 9 is the broadest of the nine and the easiest to overlook, because nothing about the code looks environment-specific — React lists it flatly as "Rendering different data on the server and the client."1

How do I find which component caused the hydration mismatch?

Read the diff. React 19 prints the path down to the mismatch and marks the divergence with + Client and - Server lines. The entry immediately above the marker is the element that diverged, and the names above that are the components that rendered it:3

  <App>
    <span>
+ Client
- Server

Tooling has caught up here. Next.js 16.2 shipped a Hydration Diff Indicator in the dev error overlay that labels the same divergence with a + Client / - Server legend, so you no longer have to read it out of the raw console output.7

Three follow-up moves close the loop quickly:

  1. Reload in a clean browser profile with no extensions installed. If the error disappears, an extension is the likeliest culprit. Do not use a private window for this test: it also drops service workers, localStorage, cookies and cached auth, any of which could be the real divergence, so a pass there proves less than it looks. Confirm by re-enabling extensions one at a time in your everyday profile.

  2. View the raw server HTML. Next.js emits the document essentially as one line, so piping it straight into grep hands you the whole page back. Break it on tags first — note that tr cannot do this, because it maps single characters and will silently no-op here:

    curl -s http://localhost:3000/your-route | perl -pe 's/>/>\n/g' | grep 'suspicious-string'
    

    That shows you exactly what the server emitted, with no browser or extension in between.

  3. Bisect with comments. Comment out half the suspect subtree, reload, repeat. This works well when the cause is stable — nesting, CSS-in-JS, whitespace. Extension and timing mismatches can be intermittent, so rule those out first or bisection will chase noise.

If you are working in a Server Components codebase and are unsure which parts even hydrate, our guide to React Server Components and the server/client boundary covers which components ship to the browser in the first place.

Why does the hydration error only appear when a browser extension is installed?

Because extensions mutate the DOM in the window between the HTML arriving and React hydrating it. A well-documented example is a Next.js discussion where ColorZilla adds cz-shortcut-listen="true" to <body>. A Next.js maintainer converted the bug report into a discussion rather than closing it, noting "As these hydration errors come from React and not Next.js, this isn't actionable as a bug report."8 Another commenter in the same thread posted a diff showing an injected id="scrnli_recorder_root" from a screen-recorder extension, and a separate Next.js discussion tracks LanguageTool's data-lt-installed attribute.89

Here is the part most write-ups get wrong. React 19 did improve extension compatibility, but the improvement is narrower than the headline suggests: "unexpected tags in the <head> and <body> will be skipped over, avoiding the mismatch errors."3 Tags — the word is doing a lot of work there. An extension that appends a <div> directly to <body> is now tolerated; an extension that adds an attribute to an element React already owns is not. And the thread kept collecting reports through React 19's stable release and on through at least November 2025 — long after the improvement shipped.8

So what do you actually do? The answer the reporter accepted was a single sentence — "add suppressHydrationWarning to the body tag" — which in an App Router root layout looks like this:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body suppressHydrationWarning>{children}</body>
    </html>
  )
}

The reporter confirmed this resolved their case.8 It is defensible here specifically because the mutation is on an element you own, one level deep, and is outside your control. One thread participant preferred a different route entirely — configuring ColorZilla to only touch the page when the extension is clicked, so no suppression is needed at all.8

Does suppressHydrationWarning actually fix a hydration mismatch?

No. It suppresses the warning for a single element whose "attribute or text content is unavoidably different between the server and the client," and React's docs attach two hard limits: "This only works one level deep, and is intended to be an escape hatch," and "React will not attempt to patch mismatched text content."1 Your users still see the server value until something re-renders it.

Two fragments, assuming an isoDate prop in scope:

// Legitimate: one element, genuinely unavoidable, no children depend on it.
// dateTime carries the machine-readable value so the markup stays valid.
<time dateTime={isoDate} suppressHydrationWarning>
  {new Date(isoDate).toLocaleTimeString()}
</time>

// Not legitimate: the mismatch is inside a child, so this suppresses nothing
<div suppressHydrationWarning>
  <UserGreeting /> {/* still throws */}
</div>

The same discussion thread has a clean counter-example. One developer reported the same hydration failure with no extensions in play, and suppressHydrationWarning did nothing. The cause was a CSS-in-JS library generating different class names on server and client; a collaborator diagnosed it as "the class names generated in server and client are diverging, that usually happens when the CSS in JS library is not setup for SSR," and the real fix was restructuring the nested class selectors.8 Suppression cannot help when the mismatch is structural.

How do I render client-only content without a hydration mismatch?

Match the server on the first client render, then update. React calls this two-pass rendering, and it is the officially documented approach:1

'use client'
import { useState, useEffect } from 'react'

export default function PublishedAt({ isoDate, serverFormatted }) {
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // First client render returns the server's string verbatim, so hydration
  // matches. The Effect then re-renders in the visitor's own locale.
  return (
    <time dateTime={isoDate}>
      {mounted
        ? new Date(isoDate).toLocaleString(undefined, { dateStyle: 'medium' })
        : serverFormatted}
    </time>
  )
}

React attaches an explicit cost to this pattern: "This approach makes hydration slower because your components have to render twice." The docs also tell you to "be mindful of the user experience on slow connections," because the JavaScript can land well after the initial HTML, and swapping the UI immediately after hydration "may also feel jarring to the user."1 Reserve it for genuinely client-only values, and prefer a server-rendered placeholder that occupies the same space as the final content so nothing shifts.

This is the right shape for a value that is read once and has no store behind it. For anything backed by a mutable browser API or an external store, the next section covers a better primitive.

Should I use useEffect or useSyncExternalStore for client-only values?

Use useSyncExternalStore whenever the value comes from a mutable browser API or a store outside React — navigator.onLine, matchMedia, a Zustand-style store. It takes a third argument, getServerSnapshot, that "runs on the server when generating the HTML" and "runs on the client during hydration," which is exactly the guarantee a hydration mismatch requires.10

'use client'
import { useSyncExternalStore } from 'react'

// Declared outside the component so React does not re-subscribe on every render.
function subscribe(callback) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)
  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

const getSnapshot = () => navigator.onLine
const getServerSnapshot = () => true // what the server HTML will say

export function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}

Two rules from the docs decide whether this works. Omit getServerSnapshot and "rendering the component on the server will throw an error." And make sure it "returns the same exact data on the initial client render as it returned on the server" — if the server preloaded store contents, you have to ship that snapshot to the client, typically via a <script> that sets a global the client reads back.10 This does not eliminate the second render. If getServerSnapshot returns true and the visitor is actually offline, React hydrates to "Online" and corrects itself immediately afterwards. What you gain over useEffect is that the server and client agree by construction rather than by you remembering to gate the first pass, plus a live subscription, with no useState scaffolding to get wrong.

How do I fix hydration mismatches caused by dates, times, and locales?

One of React's five listed triggers is squarely about time — "Date formatting in a user's locale which doesn't match the server" — and a second, "Variable input such as Date.now() or Math.random() which changes each time it's called," catches the clock as one of its two examples.3 The locale one is the sneakier, because toLocaleDateString() resolves against the runtime's locale and timezone — your server container typically runs UTC, and its locale data need not match a visitor's laptop.

// Diverges: server and client resolve locale and timezone independently
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>

// Stable: both sides are pinned to the same locale and timezone
<span>
  {new Intl.DateTimeFormat('en-GB', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: 'UTC',
  }).format(new Date(post.publishedAt))}
</span>

Pinning removes the runtime-default divergence, but it is not a guarantee of byte-identical output, because Node and browsers ship different ICU versions and ICU revisions change locale data. ICU 72 updated several locales to emit a narrow no-break space (U+202F) before AM/PM where a plain space had been, which broke string comparisons across Node versions — exactly the kind of one-codepoint difference that reads as a hydration mismatch.11 If you need a hard guarantee, format to a plain string on the server and pass it down as a prop, so only one runtime ever does the formatting.

If you genuinely want the visitor's local formatting, render the pinned version on the server and reformat in an Effect, or hand the whole element to suppressHydrationWarning as React's own timestamp example does.1 There is one adjacent gotcha worth knowing. Next.js documents that iOS "attempts to detect phone numbers, email addresses, and other data in text content and convert them into links, leading to hydration mismatches," and gives the opt-out:2

<meta
  name="format-detection"
  content="telephone=no, date=no, email=no, address=no"
/>

In the App Router you would normally emit that through the Metadata API's formatDetection field rather than hand-writing the tag, but the effect is the same.

Why does next/dynamic with ssr: false throw in the App Router?

Because in the App Router your page is a Server Component by default, and Next.js does not allow that option there. The documentation is explicit: "ssr: false option is not supported in Server Components. You will see an error if you try to use it in Server Components," and the error text you get is "ssr: false is not allowed with next/dynamic in Server Components. Please move it into a Client Component."12

// app/page.tsx — throws, because this file is a Server Component
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./chart'), { ssr: false })

Move the dynamic import into a Client Component, and render that from the page:

// app/chart-island.tsx
'use client'
import dynamic from 'next/dynamic'

// ./chart lands in the client bundle automatically, because the module that
// imports it is a Client Component. It needs no 'use client' of its own.
const Chart = dynamic(() => import('./chart'), { ssr: false })

export default function ChartIsland() {
  return <Chart />
}
// app/page.tsx — corrected: the Server Component renders the island
import ChartIsland from './chart-island'

export default function Page() {
  return <ChartIsland />
}

The same page states the constraint positively too: "ssr: false option will only work for Client Components, move it into Client Components ensure the client code-splitting working properly."12 So the pattern is a small 'use client' wrapper that owns the dynamic import, rendered from the Server Component. Skipping SSR removes the mismatch by removing the server render, which costs you the server-rendered HTML for that subtree. Treat it as a last resort for genuinely browser-only widgets, not as a general fix.

Why do useId values differ between the server and the client?

The usual answer is duller than the interesting one: the server and the client rendered structurally different trees. React generates each ID from the calling component's "parent path" through the tree rather than from an incrementing counter — which is precisely what makes the IDs stable across differing render order, and precisely why they are not stable across a differing render shape.13 A branch that renders a different shape on each side — the same typeof window and browser-API suspects from causes 2 and 3 — is enough to break that match. React's docs put the requirement plainly: useId needs an identical component tree on the server and the client. Audit those causes first.

The interesting cause is worth ruling out early anyway, because it costs one command to check: two different React versions in the same tree. React changed the default useId prefix twice: :r: in 19.0.0, «r» in 19.1.0, and _r_ in 19.2 — the last change made so generated IDs are valid for view-transition-name and XML 1.0 names.14 If your SSR bundle and your client bundle resolve to different React minors, every useId in the tree diverges and you get a wall of mismatches with no obvious application-level cause.

# Rule out a duplicated React in the install tree — the cheapest check
npm ls react react-dom

Any output showing two versions, or a react and react-dom pair that do not match, is the thing to fix first — deduplicate before looking further. This inspects the installed tree rather than what your bundles actually resolve to, so in a monorepo check your aliases and any externalised packages as well. On pnpm or Yarn the equivalent is pnpm why react or yarn why react. The related knob is the identifierPrefix root option, which React describes as "A string prefix React uses for IDs generated by useId… Must be the same prefix as used on the server."1 If you run multiple React roots on one page and set identifierPrefix on the client, set the identical value on the server renderer.

Can a CDN or edge proxy cause a hydration mismatch?

Yes, and this is the branch of the cause list that tends to reproduce in production only. Next.js lists "Incorrectly configured Edge/CDN that attempts to modify the html response" as cause 7 and names Cloudflare Auto Minify as the example.2 Any layer that rewrites HTML — minifiers, HTML post-processors, injected analytics tags — changes the bytes React hydrates against without changing your code.

Cloudflare's status here needs care, because the obvious conclusion is wrong. Cloudflare's deprecations page dates Auto Minify to August 5, 2024 and lists GET and PATCH /zones/:zone_id/settings/minify as deprecated APIs.15 Deprecated is not gone. A Cloudflare staff member explained the split on the company's own forum: "The UI was removed to prevent further activation while we work on fully retiring the backend. In the meantime… you can still manage this via the API."16 That is why Cloudflare still maintains a troubleshooting page, updated in April 2026, whose opening line is "If your site is still using deprecated features for Auto Minify, turn off Auto Minify via API."17 A zone that had it enabled before the toggle disappeared can still be minifying your HTML. Check the setting directly rather than assuming:

curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/minify" \
  --header "Authorization: Bearer <API_TOKEN>"

If any of css, html, or js comes back "on", PATCH the same endpoint to turn them off.17 The general diagnostic applies to every CDN: curl the production URL, curl your origin directly, and diff the two responses. If they differ, the edge is rewriting your HTML.

Do hydration mismatches matter in production, or only in development?

They matter in production. React's guidance is unambiguous: "React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they'll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements."1 The slowdown is everything from the nearest Suspense boundary down being re-rendered client-side, which throws away the work SSR did and can shift layout after paint. Wrapping risky subtrees in <Suspense> will not fix a mismatch, but it does cap how much of the page a mismatch can take down with it. The worst case is a correctness bug — a click landing on the wrong row.

Development and production differ in visibility, not severity. "In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches," which is why a silent production app is not evidence of a clean one.1 To see them in production, wire up the onRecoverableError root option, which React calls "when React automatically recovers from errors":1

import { hydrateRoot } from 'react-dom/client'
import App from './App'
import { yourErrorReporter } from './reporting'

hydrateRoot(document.getElementById('root'), <App />, {
  onRecoverableError: (error, errorInfo) => {
    yourErrorReporter({ error, componentStack: errorInfo.componentStack })
  },
})

That block only applies if you own the hydrateRoot call. In the Next.js App Router you do not, and Next.js does not expose onRecoverableError as a public config option — so capturing these in production means using an error-monitoring SDK that instruments the React root for you. Sentry's Next.js SDK is the usual choice; whichever you pick, confirm it reports recoverable errors and not just thrown ones, because a hydration mismatch is the former.

Bottom line

A React hydration mismatch is a determinism bug, and the list of things that break determinism is short enough to work through in one sitting. Read the diff, narrow extensions down by disabling them one at a time in your everyday profile, then walk the nine documented causes in order. For values that come from a mutable browser API or an external store, reach for useSyncExternalStore before useEffect; reach for useEffect before ssr: false; and reach for suppressHydrationWarning only when the difference is genuinely outside your control and confined to one element.

Two habits keep them from coming back: pin every date format to an explicit locale and timezone, and run npm ls react react-dom after any dependency change so your server and client renderers never drift apart.

Next, if you are debugging adjacent React rendering behaviour, our walkthrough of why React Compiler silently skips a component covers a similar class of quiet failure, and the guide to prefetching with TanStack Query in the Next.js App Router shows how to hand server-fetched data to the client without a double fetch or a mismatch.

Footnotes

  1. React, "hydrateRoot" API reference — caveats, suppressHydrationWarning, two-pass rendering, identifierPrefix and onRecoverableError. https://react.dev/reference/react-dom/client/hydrateRoot 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  2. Next.js, "Text content does not match server-rendered HTML" — the framework's numbered cause list and documented fixes. https://nextjs.org/docs/messages/react-hydration-error 2 3 4 5 6 7 8

  3. React Team, "React v19" — sections "Diffs for hydration errors" and "Compatibility with third-party scripts and extensions", published December 5, 2024. https://react.dev/blog/2024/12/05/react-19 2 3 4 5 6 7 8 9 10 11

  4. npm registry, react dist-tag latest — 19.2.8. https://registry.npmjs.org/react/latest

  5. npm registry, next dist-tag latest — 16.3.0. https://registry.npmjs.org/next/latest

  6. reactjs/rfcs, "Server errors in React 18" (RFC 0215) — how React unwinds to the nearest Suspense boundary on a hydration mismatch, and falls back to a clean client render from the root when there is none. https://github.com/reactjs/rfcs/blob/main/text/0215-server-errors-in-react-18.md 2

  7. Next.js, "Next.js 16.2" release post — Hydration Diff Indicator in the dev error overlay. https://nextjs.org/blog/next-16-2

  8. vercel/next.js Discussion #72035, "Hydration Error in Next.js 15 with React 19 Due to cz-shortcut-listen Attribute Injection from Colorzilla Extension", opened October 29, 2024. https://github.com/vercel/next.js/discussions/72035 2 3 4 5 6 7 8

  9. vercel/next.js Discussion #41816, "Warning: Extra attributes from the server: data-lt-installed". https://github.com/vercel/next.js/discussions/41816

  10. React, "useSyncExternalStore" API reference — the getServerSnapshot parameter and its server/hydration guarantees. https://react.dev/reference/react/useSyncExternalStore 2 3

  11. nodejs/node issue #46123, "Node 18.13 ICU 72 Version Breaking Change Date/Time format" — ICU 72 changed several locales to emit U+202F where a plain space had been used. https://github.com/nodejs/node/issues/46123

  12. Next.js, "How to lazy load Client Components and libraries" (docs version 16.3.0). https://nextjs.org/docs/app/guides/lazy-loading 2 3

  13. React, "useId" API reference — the Deep Dive "Why is useId better than an incrementing counter?" explains that IDs derive from the calling component's parent path, and the Pitfall notes useId requires an identical component tree on server and client. https://react.dev/reference/react/useId 2

  14. React Team, "React 19.2" — section "Update the default useId prefix", published October 1, 2025. https://react.dev/blog/2025/10/01/react-19-2 2

  15. Cloudflare, "API deprecations" — the 2024-08-05 Auto Minify entry, which deprecates GET and PATCH /zones/:zone_id/settings/minify. https://developers.cloudflare.com/fundamentals/api/reference/deprecations/ 2

  16. Cloudflare Community, "Deprecating Auto Minify" — Cloudflare staff on the UI removal versus continued API access, August 2024. https://community.cloudflare.com/t/deprecating-auto-minify/655677/18 2

  17. Cloudflare, "Turn off Auto Minify via API". https://developers.cloudflare.com/speed/optimization/content/troubleshooting/disable-auto-minify/ 2 3

Frequently Asked Questions

React rendered your tree in the browser and got different output than the HTML the server sent. It cannot attach event handlers to DOM it did not predict, so it discards the server HTML from the nearest Suspense boundary down — the whole root if there is no boundary above the mismatch — re-renders on the client, and logs one error with a diff. 3 6