# 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.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-09-18
- URL: https://diegobetto.com/en/react-hydration-mismatch/
- Tag: react, 19, hydration, ssr, server components

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.

```jsx
function Timestamp() {
  // Server renders one value, client renders a different one a moment later
  return <span>{new Date().toLocaleTimeString()}</span>;
}
```

```jsx
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:

```jsx
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.

```jsx
// 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{" "}
> <code>data-lastpass-icon-root</code>), 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

```jsx
<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**

- [Handling different client and server content – React documentation](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content)
- [suppressHydrationWarning – React documentation](https://react.dev/reference/react-dom/components/common#suppresshydrationwarning)
