skip to content

SSR and Hydration

Server rendering produces HTML the browser can paint before any JavaScript runs; hydration is the second pass that makes that HTML interactive. Interviewers use this pair to test whether you can explain time-to-first-byte, time-to-interactive, and the mismatch warnings everyone has seen.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

15

In a server-rendered React app, what is a hydration mismatch, and what does React 19 do when it detects one?

level: juniorimportance: must knowfreq 66%

answer

  1. server and client disagreed on first render
  2. the same components ran twice
  3. time, randomness, locale, browser-only reads
  4. React discards, it does not patch
  5. falls back to client rendering that subtree

basics

~20 s

A hydration mismatch means the markup React produced on the server differs from what the same components produce on the client's first render. React 19 logs a hydration error and re-renders the affected subtree on the client, throwing away that server HTML.

solid answer

~50 s

Hydration is React re-running your components in the browser over HTML the server already produced, adopting those existing DOM nodes and attaching state and event listeners instead of creating new ones. That only works if the client's first render describes exactly the same tree: same elements in the same order, same text, same attributes. When it doesn't, React reports a hydration error — in React 19 the console message includes a diff showing which server value and which client value disagreed. React does not try to patch the difference node by node. It discards the server HTML for the nearest Suspense boundary, or the whole root if there is none, and client-renders that subtree from scratch. So the practical cost is real: you paid for server rendering, then threw part of it away, and the user may see content shift as the client version replaces it.

go deeper

for a junior

Be able to say plainly that server HTML and the client's first render disagreed, and name two causes — a random or time-based value, and code that reads window or localStorage while rendering.

for a middle

Explain the mechanism: hydration adopts existing DOM rather than creating it, so it requires an identical tree, and React recovers by client-rendering the affected subtree instead of patching individual nodes.

for a senior

Show the debugging routine — ask what the server could not know, read the diff React prints, and reach for a deterministic prop or a post-mount effect. Talk about the cost: lost server work, flashes, layout shift.

for a principal

Own the prevention strategy. Decide where Suspense boundaries sit so a mismatch's blast radius is bounded, set a team rule that render must be a pure function of props and server-known data, and treat hydration errors as build- or monitoring-visible rather than console noise.

## What hydration is asserting Server-side rendering runs your components on the server and serializes the result to an HTML string. The browser parses that HTML and paints it, so the user sees content before any JavaScript has run. Then the React bundle loads and *hydration* begins: React renders the same component tree in the browser, but instead of creating DOM nodes it walks the nodes that are already there, claims them, and wires up state and event handlers. Hydration is therefore an assertion, not a merge. React is saying: *the tree I am producing right now is the same tree that produced this HTML.* A hydration mismatch is that assertion failing. ## What counts as a mismatch Anything React can compare while walking the existing DOM: - different text content in a node, - a different element type at a given position (`<span>` where the HTML has `<div>`), - an extra or missing child, or children in a different order, - differing attributes on an element. ## Why the same components produce two different trees The component code is identical, so something outside the code differs between the two runs: - **Nondeterministic values.** `Date.now()`, `new Date()`, `Math.random()`, a module-level counter used to build ids. The server run and the client run simply produce different numbers. - **Environment differences.** The server usually runs in UTC with a server-side locale; the browser has the user's timezone and locale. Any `toLocaleString`/`Intl` formatting can differ even when the underlying value is identical. - **Browser-only reads.** `window`, `document`, `localStorage`, `matchMedia`, `navigator.userAgent`. These do not exist on the server, so code that branches on them (`typeof window !== 'undefined' ? a : b`) renders one thing on the server and the other on the client — a self-inflicted mismatch. - **Invalid HTML nesting.** The browser's parser silently restructures illegal markup, so the DOM React finds is not the DOM the server string described. - **Outside mutation.** Browser extensions or third-party scripts that inject or rewrite nodes before hydration. ## What React 19 does about it React logs a hydration error to the console. React 19 improved this message specifically: it prints a diff of the offending subtree marking which parts came from the server and which from the client, instead of the older "text content did not match" one-liner. Then it recovers by *client rendering*. React does not patch individual text nodes to make them agree; it drops the server-rendered DOM for the smallest enclosing Suspense boundary — or the entire root when the mismatch is not inside one — and renders that subtree fresh on the client. The application ends up correct, which is why a mismatch is a console error rather than a blank page, but you lost the server work for that region and the user can see a visible replacement or layout shift. That recovery behaviour is also why Suspense boundary placement matters for blast radius: a mismatch deep inside a boundary costs you that boundary, not the page. ## Fixing one, in three shapes **Make the render deterministic.** Compute the varying value once, on the server, and pass it down as a prop — a timestamp as an ISO string, a locale from the request — so both runs read the same input. This is the best fix when it is available. **Render the neutral value, correct after mount.** Render whatever the server can know, then update in an effect, which runs only in the browser after hydration has succeeded: ```jsx function Timestamp({ iso }) { const [text, setText] = useState(iso); // both renders agree here useEffect(() => { setText(new Date(iso).toLocaleString()); // browser-only, after hydration }, [iso]); return <time dateTime={iso}>{text}</time>; } ``` This costs one extra client render and a brief flash of the server value, and it is correct by construction. **Tell React you expect the difference.** `suppressHydrationWarning` on the element silences the report for that element's own text and attributes. It is an escape hatch for genuinely unavoidable differences, not a fix. ## The diagnosis habit When you hit one, ask a single question: *what did the server not know?* The answer is nearly always time, timezone, locale, randomness, or something only the browser can read. That short list is why interviewers like this question — a candidate who has actually debugged SSR names the categories immediately instead of guessing.

  • How much of the page does React throw away when hydration fails?
    Not the whole app by default — React client-renders the nearest enclosing Suspense boundary. If the mismatch happens outside any boundary, the fallback is the entire root. That makes boundary placement a blast-radius decision: wrapping a risky, client-dependent region in its own Suspense boundary keeps a mismatch there from costing you the rest of the server-rendered page.
  • Why not just let React patch the difference instead of re-rendering the subtree?
    Because React cannot tell whether the difference is cosmetic or structural. If the trees diverge, the DOM nodes React would adopt may not correspond to the elements it thinks it is adopting, so state, refs and listeners would attach to the wrong nodes. Re-rendering the subtree from the client's own description guarantees consistency; patching would guarantee only that the text looks right.
  • Is a hydration mismatch ever harmless enough to ignore?
    Rarely. Even the cosmetic-looking ones cost you the server rendering for that subtree and can cause a visible flash or layout shift, and mismatches often travel in pairs with a real bug — a component reading browser state during render. Treat the error as actionable and fix the source; reach for suppressHydrationWarning only for a difference you genuinely intend, like a rendered local timestamp.

saying these in an interview costs you the question

  • Says React silently patches the text to match the client
  • Claims mismatches only happen in development builds
  • Blames React's diffing algorithm rather than nondeterministic render input
  • Thinks the fix is to disable server rendering for the whole page
  • Says the warning is cosmetic and can be ignored in production

context

open as a page

A React app serves server-rendered HTML, then the JavaScript bundle loads and calls hydrateRoot. What does React actually do to that existing HTML during hydration, and what does it deliberately not do?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Hydration reuses the server's DOM instead of rebuilding it: React renders the component tree on the client, matches it against the existing nodes, attaches event listeners at the root container, fills refs, then commits and runs effects.

open as a page

In React 19, what is the difference between calling hydrateRoot(container, <App />) and createRoot(container).render(<App />), and what goes wrong if you use createRoot on a container that already holds server-rendered HTML?

level: middleimportance: must knowfreq 58%

basics

~10 s

createRoot renders a fresh tree into a container; hydrateRoot adopts server-rendered HTML already inside it, reusing those nodes. Calling createRoot on server HTML discards that markup and re-renders from scratch, wasting the server work.

open as a page

A server-rendered React component renders <span>{new Date().toLocaleTimeString()}</span>, and the browser console reports a hydration mismatch on every page load. Explain the cause and how you would show the local time without the mismatch.

level: middleimportance: must knowfreq 72%

basics

~20 s

The server and browser call new Date() at different moments, in different timezones and locales, so the two renders produce different text. Render a stable server-provided value first and switch to the localized time in an effect after mount.

open as a page

A React page rendered on the server with react-dom/server's renderToString contains a Suspense boundary whose child awaits a slow database query. What HTML does the browser receive, and what changes if you switch to renderToPipeableStream?

level: middleimportance: must knowfreq 62%

basics

~20 s

renderToString buffers the whole page and never waits for suspended data, so the boundary ships as its fallback and the response leaves only after the full render. renderToPipeableStream flushes that same shell immediately, then streams the real content on the same response when the query resolves.

open as a page

In react-dom/server, what is the difference between renderToString and renderToStaticMarkup, and when would you reach for the static one?

level: juniorimportance: should knowfreq 40%

basics

~20 s

renderToString emits React's hydration bookkeeping — comment markers around Suspense boundaries and separators between adjacent text nodes — so the client can attach to the markup. renderToStaticMarkup omits all of it, producing plain HTML for output React will never hydrate.

open as a page

A server-rendered React component renders a <div> inside a <p>. The markup uses no dates, randomness or browser APIs, yet hydration still reports a mismatch. What causes it?

level: middleimportance: should knowfreq 44%

basics

~20 s

The browser's HTML parser cannot nest a <div> inside a <p>, so it closes the paragraph early and makes the div a sibling. React then hydrates against a DOM whose shape differs from the tree it is rendering, and reports a mismatch.

open as a page

What does React's suppressHydrationWarning attribute do to a server-rendered element, how far does its effect reach, and when is reaching for it the right call?

level: middleimportance: should knowfreq 38%

basics

~20 s

suppressHydrationWarning tells React not to report differences in that one element's text and attributes between the server HTML and the first client render. It reaches only one level deep, changes nothing about the rendering itself, and is an escape hatch for differences you intend.

open as a page

react-dom/server exposes both renderToPipeableStream and renderToReadableStream. How do the two differ, and how do you choose between them?

level: middleimportance: should knowfreq 45%

basics

~20 s

They produce the same streamed HTML for different runtimes. renderToPipeableStream targets Node.js streams: it returns { pipe, abort } synchronously and you pipe into the server response. renderToReadableStream targets Web Streams: it returns a promise for a ReadableStream you pass to a Response.

open as a page

A server-rendered React app chooses its theme by reading localStorage during render. In the browser it briefly shows the light theme and logs a hydration mismatch. Explain the cause and the options you would consider for fixing it.

level: seniorimportance: should knowfreq 50%

basics

~20 s

localStorage does not exist on the server, so the server rendered a default theme while the browser's first render read the stored one — two different trees. Fix it by rendering a server-known default and applying the stored theme after hydration, or by setting it before hydration outside React's tree.

open as a page

What is the onRecoverableError option of React's hydrateRoot and createRoot, when does React call it, and what does "recoverable" mean for the page the user is looking at?

level: seniorimportance: should knowfreq 32%

basics

~20 s

onRecoverableError is an option of hydrateRoot and createRoot that React calls when it hit an error and recovered by itself — typically a hydration error it fixed by re-rendering that content on the client. The UI is correct, so wire it to your error reporter.

open as a page

On a server-rendered React 19 page, a user clicks a button before hydration has finished. What happens to that click, and what does React do about interactions that arrive during hydration?

level: seniorimportance: should knowfreq 46%

basics

~20 s

If hydrateRoot has already started, React captures the click on the root container, prioritizes hydrating the subtree the user touched, and replays the event once that subtree is ready. If the bundle has not executed yet, no listener exists and the click is lost.

open as a page

When streaming a React app with react-dom/server's renderToPipeableStream, what is the difference between the onShellReady and onAllReady callbacks, and how does that choice affect the HTTP status code you can send?

level: seniorimportance: should knowfreq 38%

basics

~20 s

onShellReady fires when everything outside Suspense boundaries has rendered; piping there streams and locks the status code immediately. onAllReady fires only after every boundary has resolved; piping there gives up streaming but keeps the status changeable until the whole page is known good.

open as a page

A server-rendered React page paints in under a second but stays unresponsive for several seconds afterwards while hydration runs. As the engineer who owns this page, how do you reason about hydration cost, and what tradeoffs do you weigh in reducing it?

level: principalimportance: should knowfreq 30%

basics

~20 s

Hydration cost scales with the size of the client component tree, not with what the user can see. Reduce it by keeping non-interactive parts out of that tree and splitting what remains so interactive regions hydrate first, then verify with real-user interaction data.

open as a page

A React server render has already streamed its shell, but one Suspense boundary is still waiting on data five seconds later. How do you cut the render short with react-dom/server's streaming APIs, and what does the browser end up with?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Set a timer and call the abort function that renderToPipeableStream returns, or abort the AbortController whose signal you passed to renderToReadableStream. React stops server work, sends the unresolved boundaries' fallbacks, closes the stream, and the client renders those regions itself.

open as a page