Command Palette

Search for a command to run...

Hectal
PHASE 20Advanced React 18+ ~23 min· topic 6 of 7

Topic 20.6

Hydration and Hydration Errors

In one line

Hydration is React rendering the same tree in the browser and adopting the server's HTML instead of creating new DOM; if the first client render produces different output (dates, random values, browser-only data, invalid nesting), React reports a hydration mismatch, so make the first client render match the server and change things after hydration.

Think of it like this

A paint-by-numbers kit. The server sends a printed outline (the HTML). In the browser, React gets the same numbered instructions (your components) and checks each region: "number 3 here, yes, it's there", and then adds the colour (event handlers and state). If the instructions say a cat but the printed outline shows a dog, React can't colour it in; it has to throw the page away and draw the cat itself. That's a hydration mismatch.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Hydration
React adopting server-rendered HTML in the browser and attaching state and event handlers to it.
hydrateRoot
The react-dom/client function that starts hydration on a container that already has server HTML.
Hydration mismatch
When the first browser render differs from the HTML the server sent.
Recoverable error
An error React can fix by re-rendering on the client; it's reported but the app keeps working.
useId
A hook that makes an id that is the same on the server and the client, for linking labels and inputs.
suppressHydrationWarning
An attribute that silences mismatch warnings for one element's own text and attributes.
Two-pass rendering
Rendering the server-safe version first, then the browser-specific version after hydration in an effect.
Invalid nesting
HTML elements placed where the HTML rules don't allow, like a <div> inside a <p>.

Step by step

01Server renders, browser claims

The server sends HTML. The browser loads React and the same components, and React matches its first render to the existing nodes one by one.

Server renders, browser claimsdiagram
Rendering diagram…

02Spot the classic bug

This component renders the current time. The server renders at 10:00:00, the browser hydrates at 10:00:02: the text differs, so hydration fails.

Clock.jsxwhole filejsx
function Clock() {
  return <p>It is {new Date().toLocaleTimeString()}</p>; // differs every render
}

03Fix it with two passes

Render something stable first (empty, or a server-provided time), then fill in the browser's value after hydration. Effects never run on the server.

Clock.jsxwhole filejsx
function Clock() {
  const [time, setTime] = useState(null); // same on server and first client render
  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
    const id = setInterval(() => setTime(new Date().toLocaleTimeString()), 1000);
    return () => clearInterval(id);
  }, []);
  return <p>It is {time ?? "..."}</p>;
}

04Or mark a known difference

When a small difference is expected and harmless, suppressHydrationWarning on that one element tells React not to complain. React keeps the server's text until the next update.

Posted.jsxwhole filejsx
<time dateTime={post.iso} suppressHydrationWarning>
  {new Date(post.iso).toLocaleString()}
</time>

05Use useId for ids

Ids made with a counter or Math.random() differ between server and client. useId is derived from the component's place in the tree, so both sides agree.

Field.jsxwhole filejsx
function Field({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

Try it yourself

  1. 1

    Make the mismatch an attribute

    In the first example, render <p title={onServer ? "a" : "b"}>Same text</p> instead. Predict: error or warning? Is the title fixed afterwards? (Warning, and no.)

  2. 2

    Remove the fix

    In the second example, change useState(null) to useState(localStorage.getItem("name")) and run. The server can't read the browser's storage, so the first client render differs: you get the hydration error.

  3. 3

    Suppress one level only

    Move suppressHydrationWarning from <time> to the outer <div>. Does the warning for the <p> go away? (No: it only covers the element's own attributes and direct text.)

Code & diagrams

A real hydration mismatch New tab

Simulated server (a flag instead of a real server), real React error. React prints the diff (`+` what the client rendered, `-` what the server sent) and re-renders on the client, so the screen ends up right but the server HTML was wasted.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Uncaught Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a 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

  <Where>
    <p>
+     Rendered in the browser
-     Rendered on the server

Screen: Rendered in the browser
Fixing it: effect after hydration, or a server snapshot New tab

No warnings this time: both components render the server's version during hydration and switch to the browser's value right after. `localStorage` here is the preview's in-memory copy.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

server HTML: Welcome!Times shown in your time zone
after hydration: Welcome back, Asha | Times shown in UTC+5:30
Attribute mismatches aren't patched; suppressHydrationWarning New tab

The `className` difference only warns, and the DOM keeps `server`: React will not fix attributes during hydration. The `<time>` difference is silenced by `suppressHydrationWarning`, and its server text also stays until the next update.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a 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

  <App>
    <div>
      <p
+       className="client"
-       className="server"
      >
+       attribute
      ...

p class: server
time text: server time
useId matches on server and client React 18+ New tab

The exact id format is an internal detail (it changed between React versions); treat ids as opaque strings. What matters is that both sides agree.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

server ids: [ '_R_1_', '_R_2_' ]
client ids: [ '_R_1_', '_R_2_' ]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Put a <div> inside a <p>

Render <p>Intro <div>block</div></p> in any component (no SSR needed).

terminal
$ Console
── what you'll see ──
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.
 
<App>
> <p>
> <div>

Break #2

Generate ids with a counter

Write let next = 0; function Field() { const id = "field-" + next++; ... } and server-render it.

terminal
$ Console
── what you'll see ──
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. …
 
<Field>
<div>
<label
+ htmlFor="field-1"
- htmlFor="field-0"
>
…

Myth vs fact

Myth

Hydration re-creates the page from scratch.

Fact

It reuses the server's DOM nodes and only attaches behaviour. It does run all your components again in the browser, which costs CPU time.

Myth

A hydration warning is harmless because the page still looks right.

Fact

Text mismatches throw away server HTML (slower, can flash). Attribute mismatches are never fixed, so the DOM can disagree with React state until the next update.

Myth

suppressHydrationWarning fixes the mismatch.

Fact

It only hides the message for one element. The server's value stays on screen until something re-renders that element.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Hydration errors are reported through hydrateRoot's onRecoverableError option (default: reportError, which is why the preview shows them as Uncaught Error). Send them to your error tracker; in production React prints shorter messages with an error code.

  • ▸

    Mismatch recovery is per Suspense boundary: the nearest boundary above the mismatch is client-rendered. Wrapping risky parts in their own <Suspense> limits the damage.

  • ▸

    React 19 tolerates unexpected tags inserted by extensions or third-party scripts in <head> and <body> (it skips them instead of failing), but changes inside your components' DOM still cause mismatches.

  • ▸

    useSyncExternalStore without a getServerSnapshot throws during server rendering; with one, React uses it on the server and during hydration, then re-renders with the client snapshot, which is the cleanest way to read browser stores in SSR apps.

Remember this

  1. 1

    How hydration works. hydrateRoot(container, <App />) renders your components in the browser as if for the first time, but instead of creating DOM nodes it walks the existing server HTML in the same order and claims each node: this <button> belongs to that element, this text node to that string. Then it attaches event listeners, runs effects and the page becomes interactive. The DOM is reused, so there's no flash and inputs keep what was typed. For this to work, the first client render must produce exactly the same output as the server did.

  2. 2

    What happens on a mismatch. If the text or elements differ, React 19 throws away the server HTML up to the nearest Suspense boundary (or the whole root) and renders it again on the client: you see an error in the console (Hydration failed because the server rendered text didn't match the client...) and lose the benefit of SSR there. If only attributes differ (a className, a style), React logs a warning and does not fix them: the page keeps the server's attribute while React believes it set the client's value, a subtle bug. In both cases React 19 prints a diff (+ client, - server) to help you find it.

  3. 3

    Common causes. (1) Time: new Date().toLocaleTimeString() differs between the server render and the hydration moments later. (2) Randomness: Math.random() or a random id. (3) Locale and time zone: formatting a date or number on a server in UTC and en-US versus a browser in Asia/Kolkata and hi-IN. (4) Browser-only data: window.innerWidth, localStorage, navigator.language, or code like typeof window !== "undefined" ? A : B inside render. (5) Invalid HTML nesting: <p><div/></p> or <a> inside <a>; the browser's HTML parser rearranges the server HTML, so the DOM no longer matches what React expects. (6) Browser extensions that change the page before React loads (password managers, translators), which you can't fully control.

  4. 4

    Fixes. For browser-only values, render the server version first and switch after hydration: keep the value in state, initialise it to what the server knows, and set the real one in useEffect (which never runs on the server); or use useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) (Topic 9.6), which uses the server snapshot during hydration automatically. For time and locale, pass the value or the time zone from the server as data, or format dates in a fixed time zone. For random ids that connect labels and inputs use **useId()**, which produces the same id on server and client because it's based on the component's position in the tree. Fix invalid nesting in the markup (React warns about it during development even without SSR).

  5. 5

    **suppressHydrationWarning.** Adding suppressHydrationWarning={true} to an element silences the mismatch warning for that element's own text and attributes, one level deep. It's an escape hatch for unavoidable differences like a timestamp (<time suppressHydrationWarning>) or a theme class set by a script before React loads (<html suppressHydrationWarning>, which is how theme libraries avoid a flash). The server's value stays on screen until the next render; it doesn't make React patch it. Never use it to hide real bugs.

  6. 6

    Client-only rendering. Some components can't be server-rendered at all: a map library that touches window on import, a chart that measures the screen. Options: render a placeholder until mounted (the effect pattern), wrap them in <Suspense> and load them lazily on the client, or in Next.js use dynamic(() => import("./Map"), { ssr: false }) from next/dynamic inside a client component. Also see Topic 20.1: React 18+ hydrates Suspense boundaries independently, so a mismatch inside one boundary only re-renders that boundary.

Explain it without notes

01

Explain step by step what hydrateRoot does.

02

List five causes of hydration mismatches and the fix for each.

03

What's the difference between a text mismatch and an attribute mismatch in React 19?

04

When is suppressHydrationWarning appropriate?

Practice

01

Rewrite function Width() { return <p>{window.innerWidth}px</p>; } so it works with SSR without a mismatch.

02

A server in UTC renders new Date("2024-01-15T10:30:00Z").toLocaleString(). Why can it mismatch, and how do you make it stable?

Trade-offs

  • ↔

    Two-pass rendering avoids mismatches but shows a placeholder briefly and causes an extra render; reserve space so the layout doesn't jump.

  • ↔

    Client-only components avoid SSR problems entirely but lose SSR's benefits for that part (no HTML for crawlers, later first paint).

Done when you can

  • I can explain how hydration reuses server HTML.

  • I can read React's hydration diff and find the cause.

  • I can fix mismatches from time, randomness, locale, browser APIs and invalid nesting.

  • I use useId for ids that must match on both sides.

  • I know when suppressHydrationWarning is acceptable and what it doesn't do.

  • I can render a component only on the client when I must.