Skip to the solution
HardSystem Design8 min read

What is hydration in React?

322 views

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.

  • ssr
  • hydration
  • performance

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 11 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA hydration mismatch and the mounted-flag fix, both live
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 106 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

How does React 19 handle document metadata like `<title>` and `<meta>`?

Continue