Skip to solution
hardFrontend

What causes hydration mismatches, and how do you handle them?

305 views
01

Understand the problem

Question presented to candidate: "Your logs are full of hydration warnings. How do you work out what is causing them and what do you do about each kind?"

What a strong answer should cover:

  • A mismatch is the client's first render producing something different from the server HTML. React then discards the mismatched subtree and re-renders it on the client.
  • Not all mismatches are equal, which is the key insight: a text or structure mismatch is a recoverable error and the client value wins; an attribute mismatch only warns in the console and the server value is kept.
  • The causes fall into four groups: non-deterministic output (Date, Math.random, generated ids), environment differences (locale, timezone, window, localStorage), invalid HTML nesting the browser silently repairs, and third parties — browser extensions injecting markup.
  • The fix for a genuinely client-only value is a mounted flag: render the neutral version on the server and the first client render, then switch. Checking typeof window during render does not work — it makes the first client render differ, which is the mismatch.
  • For a store-backed value, useSyncExternalStore takes a server snapshot for exactly this.
  • suppressHydrationWarning is for one unavoidable node, such as a timestamp. It silences the warning and keeps the server value — it does not make the two agree.
  • Extension-caused mismatches are not your bug; recognise them rather than chasing them.

Clarifying questions expected:

  • "Is the differing value genuinely client-only, or just accidentally non-deterministic?" — different fixes.
  • "Does the warning reproduce in a clean profile with extensions disabled?"

Code / implementation expected: Optional. The mounted-flag pattern is three lines and worth writing out.

reactssrhydration
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 senior React interviews — assumes what hydration is. Difficulty: Hard

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

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Four mismatches triggered live, showing which value survives each
Run Playground
import { useState, useEffect } from "react";
import { renderToString } from "react-dom/server";
import { hydrateRoot } from "react-dom/client";

// Server renders one thing, client another. Each case reports whether React
// raised a recoverable error and WHICH value ended up in the DOM.
const CASES = [
  {
    name: "text mismatch",
    Server: () => <p>server text</p>,
    Client: () => <p>client text</p>,
    read: (host) => "text = " + JSON.stringify(host.textContent),
  },
  {
    name: "attribute mismatch",
    Server: () => <p className="from-server">same text</p>,
    Client: () => <p className="from-client">same text</p>,
    read: (host) => "class = " + JSON.stringify(host.querySelector("p").className),
  },
  {
    name: "extra element on client",
    Server: () => <div><span>a</span></div>,
    Client: () => <div><span>a</span><span>b</span></div>,
    read: (host) => "text = " + JSON.stringify(host.textContent),
  },
  {
    name: "suppressed mismatch",
    Server: () => <p suppressHydrationWarning>server value</p>,
    Client: () => <p suppressHydrationWarning>client value</p>,
    read: (host) => "text = " + JSON.stringify(host.textContent),
  },
];

export default function App() {
  const [rows, setRows] = useState([]);

  const run = async () => {
    const out = [];
    for (const c of CASES) {
      const host = document.createElement("div");
      // Produce the "server" HTML, exactly as a server would.
      host.innerHTML = renderToString(<c.Server />);
      document.body.appendChild(host);

      const errors = [];
      const warnings = [];
      const origErr = console.error;
      console.error = (...a) => warnings.push(String(a[0]));

      hydrateRoot(host, <c.Client />, {
        onRecoverableError: (e) => errors.push(String(e.message).split(".")[0]),
      });
      // Let hydration settle before reading the result.
      await new Promise((r) => setTimeout(r, 60));
      console.error = origErr;

      out.push({
        name: c.name,
        errors: errors.length,
        warnings: warnings.length,
        winner: c.read(host),
      });
      host.remove();
    }
    setRows(out);
  };

  useEffect(() => { run(); }, []);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 580 }}>
      <button onClick={run} style={{ marginBottom: 12 }}>re-run</button>

      <table style={{ width: "100%", fontSize: 13, borderCollapse: "collapse" }}>
        <thead>
          <tr style={{ textAlign: "left", borderBottom: "1px solid #ccc" }}>
            <th>case</th><th>recoverable</th><th>warnings</th><th>what survived</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((r) => (
            <tr key={r.name} style={{ borderBottom: "1px solid #eee" }}>
              <td style={{ padding: "4px 8px 4px 0" }}>{r.name}</td>
              <td style={{ color: r.errors ? "#a33" : "#161" }}>{r.errors}</td>
              <td style={{ color: r.warnings ? "#a60" : "#161" }}>{r.warnings}</td>
              <td><code style={{ fontSize: 12 }}>{r.winner}</code></td>
            </tr>
          ))}
        </tbody>
      </table>

      <p style={{ fontSize: 13, color: "#666" }}>
        Read the second row against the others. Text and structure mismatches
        raise a recoverable error and the <strong>client</strong> value wins.
        The attribute mismatch raises none — a console warning only — and the
        <strong> server</strong> class is kept. That is why a theme class that
        differs between server and client gets stuck on the server value and
        never shows up in your error reporting.
      </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 108 of 119 decoded in the React.js track. One more won't hurt.

Back to track