Skip to solution
hardSystem Design

What is hydration in React?

322 views
01

Understand the problem

Question presented to candidate: "Your server-rendered page appears instantly but clicking does nothing for a second. What is happening in that gap?"

What a strong answer should cover:

  • The server HTML is inert — it has no event handlers. Hydration is the client-side pass that adopts that existing DOM and attaches behaviour to it.
  • hydrateRoot reuses the existing nodes; createRoot would throw them away and rebuild, losing the entire benefit of SSR.
  • React walks the tree and matches its rendered output against the DOM it finds, wiring up state, refs and event delegation onto nodes that already exist.
  • The gap the question describes is exactly that window: painted but not yet hydrated, and it is bounded by bundle download and parse, not by the server.
  • A mismatch — server and client rendering different things — makes React discard the mismatched subtree and re-render on the client, which is slow and can visibly flicker.
  • Common mismatch causes: Date/Math.random() in render, locale or timezone formatting, reading window/localStorage during render, and invalid HTML nesting the browser silently repairs.
  • The fix for genuinely client-only values is to render a neutral placeholder on the server and fill it in after mount, or use useSyncExternalStore with a server snapshot.
  • Partial and selective hydration (Suspense boundaries, Server Components, islands) shrink the gap by hydrating less, or later.

Clarifying questions expected:

  • "Is the delay bundle download, or hydration itself?" — different fixes.
  • "Are there any hydration warnings in the console?"

Code / implementation expected: Optional. Showing the mismatch and the mounted-flag fix is the useful pair.

ssrhydrationperformance
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes what SSR is. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every claim

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A hydration mismatch and the mounted-flag fix, both live
Run Playground
import { useState, useEffect, useSyncExternalStore } from "react";
import { renderToString } from "react-dom/server";

// ❌ Renders a different value every time it runs. On the server it produces
//    one number; on the client, another. That IS a hydration mismatch.
function Unstable() {
  return <code>id-{Math.floor(Math.random() * 1000)}</code>;
}

// ✅ The first client render matches the server exactly, and the real value
//    appears on the render AFTER mount. Note that checking typeof window
//    during render would NOT work — that makes the first client render differ.
function Stable() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return <code>{mounted ? new Date().toLocaleTimeString() : "--:--:--"}</code>;
}

// ✅ The store version: the third argument is the SERVER snapshot, which is
//    what the server render and the first client render both use.
const widthStore = {
  subscribe(cb) {
    window.addEventListener("resize", cb);
    return () => window.removeEventListener("resize", cb);
  },
  get: () => window.innerWidth,
  getServer: () => 0,
};

function Width() {
  const w = useSyncExternalStore(widthStore.subscribe, widthStore.get, widthStore.getServer);
  return <code>{w === 0 ? "unknown on the server" : w + "px"}</code>;
}

// Two server renders of the unstable component, done at MODULE scope: calling
// a server renderer during a client render nests one React render inside
// another and throws. Note the two outputs differ — that is the whole problem.
const a = renderToString(<Unstable />);
const b = renderToString(<Unstable />);
const stableServer = renderToString(<Stable />);

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.8, maxWidth: 560 }}>
      <h4 style={{ margin: "0 0 4px" }}>❌ non-deterministic render</h4>
      <pre style={{ background: "#fee", padding: 8, borderRadius: 6, fontSize: 12 }}>
{"server pass 1: " + a + "\nserver pass 2: " + b}
      </pre>
      <p style={{ fontSize: 13, color: "#666", margin: "0 0 16px" }}>
        Two server renders, two different ids. The client would produce a third,
        React would report a recoverable error, and that subtree would be thrown
        away and re-rendered. Live now: <Unstable />
      </p>

      <h4 style={{ margin: "0 0 4px" }}>✅ neutral on the server, real after mount</h4>
      <pre style={{ background: "#eef7ee", padding: 8, borderRadius: 6, fontSize: 12 }}>
{"server output: " + stableServer}
      </pre>
      <p style={{ fontSize: 13, color: "#666", margin: "0 0 16px" }}>
        The placeholder is what both the server and the FIRST client render
        produce, so they match. Live now: <Stable />
      </p>

      <h4 style={{ margin: "0 0 4px" }}>✅ external store with a server snapshot</h4>
      <p style={{ fontSize: 13, color: "#666" }}>
        Live now: <Width /> — the third argument to{" "}
        <code>useSyncExternalStore</code> is what the server uses, which is why
        this can read <code>window</code> without a mismatch.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 106 of 119 decoded in the React.js track. One more won't hurt.

Back to track