Diego Betto
Photo by Akira Hojo on Unsplash

Diego Betto · September 18, 2026 · 5 min di lettura

React 19 hydration mismatch: why "Text content does not match server-rendered HTML"

The three real causes of hydration errors in SSR React apps, how to read React's dev diff, and when suppressHydrationWarning is actually the right call.

Condividi:XLinkedInFacebookWhatsApp

You render the same component on the server and the client — same props, same code — and yet the browser console shows this the moment the page loads:

⛔ Error

Hydration failed because the server rendered HTML didn’t match the client. As a result this tree will be regenerated on the client, which can affect performance.

Or, depending on the framework and React version, a more specific variant:

⛔ Error

Text content does not match server-rendered HTML.

Hydration is React attaching event handlers and internal bookkeeping to HTML that was already rendered on the server — it doesn’t re-render from scratch, it reuses the existing DOM. That only works if what React would render on the client is byte-for-byte identical to what the server actually sent. When it isn’t, you get this error.

Cause #1: values that differ between server and client

The classic offender: anything that produces a different result depending on where it runs.

function Timestamp() {
  // Server renders one value, client renders a different one a moment later
  return <span>{new Date().toLocaleTimeString()}</span>;
}
function Greeting() {
  // Math.random(), navigator.language, or anything reading window/localStorage
  // has no defined value during SSR and a real one on the client
  const id = Math.random();
  return <div data-id={id}>Hello</div>;
}

The fix is to either compute the value only on the client (inside a useEffect, after the initial hydration-matching render) or pass it down from the server as a prop, so both environments render the exact same thing:

function Timestamp() {
  const [time, setTime] = useState(null);

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);

  // Renders nothing (or a stable placeholder) on both server and first client render
  return <span>{time ?? "--:--:--"}</span>;
}

Cause #2: invalid HTML nesting

Browsers silently “fix” invalid HTML while parsing it — moving elements around in ways your React tree never asked for. The server sends the invalid markup as-is, the browser reinterprets it during parsing, and React’s client-side render doesn’t match what actually ended up in the DOM.

// Invalid: <div> is not allowed inside <p> per the HTML spec
function Card() {
  return (
    <p>
      <div>Some content</div>
    </p>
  );
}

The browser silently closes the <p> before the <div>, producing a DOM structure React didn’t render. This is easy to introduce accidentally — a <Card> component rendering a <div> used inside a <Text> component that wraps children in a <p>, for instance.

Cause #3: browser extensions modifying the DOM before hydration

This one isn’t your bug at all. Some browser extensions (password managers, ad blockers, accessibility tools) inject attributes or elements into the page before React hydrates — React then sees a DOM that doesn’t match what it expects and complains, even though your code is correct.

ℹ️ How to tell

If the mismatch only happens for some visitors, only in production (not your local dev environment), or the reported diff shows an attribute you never wrote (like data-lastpass-icon-root), it’s very likely an extension. Try reproducing in an incognito window with extensions disabled before spending time debugging your own code.

Reading React’s hydration diff

In development, React shows a diff of what the server sent versus what the client would render — read it the same way you’d read a git diff: the - line is what the server produced, the + line is what the client wants to produce. Whichever value differs is your starting point for narrowing down cause #1 above.

When suppressHydrationWarning is legitimate

<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>

This tells React “I know this specific element’s text will differ between server and client, don’t warn about it, and don’t re-render it during hydration.” It’s the right tool exactly when a value is genuinely expected to differ (a live clock, a randomly generated ID that doesn’t affect layout) and you’ve deliberately decided the mismatch is harmless. It’s the wrong tool as a way to silence an error you haven’t actually diagnosed — it only suppresses the warning on that one element, not the underlying inconsistency, and it doesn’t fix invalid HTML nesting or a genuinely broken value at all.

FAQ

❓ Does this only happen with Next.js?

No — it’s a property of any React app doing server-side rendering, including Astro islands, Remix, and custom SSR setups. The cause and fix are the same regardless of framework.

❓ Does React Server Components make this more or less likely?

It changes where the risk shows up rather than removing it. Server Components themselves don’t hydrate (they never run in the browser), but any Client Component they render into still needs to match between the server-rendered HTML and the client’s first render, so the same three causes above still apply to that boundary.

❓ Is a hydration mismatch a performance problem, not just a bug?

Both. When React detects a mismatch, it throws away the mismatched subtree and re-renders it entirely on the client — which means you lose the benefit of SSR (fast first paint) for that part of the page, on top of the console error.

Conclusion

Hydration errors always come down to the client rendering something different from what the server sent — a value that legitimately differs by environment, HTML the browser silently restructured, or a browser extension injecting into the DOM before React gets there. suppressHydrationWarning is a scalpel for the first case when the difference is truly expected and harmless, not a general-purpose fix — diagnose which of the three you’re looking at before reaching for it.

References

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.