← Back to question bank
DebuggingSeniorHard#4036 · 35m

Debug hydration mismatch debugging under interview pressure

A React screen using hydration mismatch debugging passes simple tests but breaks during repeated interaction. Find the likely root cause, patch it, and describe the longer-term design improvement.

Answer Strategy

The broken pattern in this question is "read a non-deterministic value during render". Date.now, Math.random, window.innerWidth, navigator.userAgent, and localStorage are the canonical offenders. The server renders one value, the client first render produces a different value, and React’s hydration check fires "Text content does not match server-rendered HTML". The patched DOM loses event handlers and the page degrades silently. The fix is two patterns: server-injected initial state, or a ClientOnly gate.

Locate the boundary by asking "is this value knowable on the server?". If yes, the server renders it and passes it as a prop or initial state; the client uses the same value on first render and upgrades inside useEffect. If no (geolocation, viewport, user-agent-derived values), wrap the dependent UI in ClientOnly so the server emits a fallback the client can also emit on first render.

Adjacent traps: relying on suppressHydrationWarning (it hides the warning, not the broken event handlers underneath), using typeof window === 'undefined' branches in render (server and client first render both lack the post-mount flag, so the conditional still mismatches), and forgetting useId for SSR-stable a11y ids (Math.random-based ids cause a different hydration mismatch in role/aria attributes).

Regression Fix: Server-Safe Initial State + Effect Upgrade

The fixed Clock takes the server’s timestamp as a prop and upgrades to live time inside useEffect after hydration commits.

// THE BUG: the component called Date.now() during render. Server emitted
// timestamp T0; client first render emitted T1 (different by a few
// hundred ms). React patched the DOM, lost event handlers in the
// patched range, and warned "Text content does not match server-rendered
// HTML". The fix is to render the server-injected initial value first
// and upgrade to live time inside useEffect.

type ClockProps = { serverNow: number };

export function Clock({ serverNow }: ClockProps) {
  // Initial value is the server's snapshot — guaranteed identical on
  // both sides of hydration.
  const [now, setNow] = React.useState(serverNow);
  React.useEffect(() => {
    setNow(Date.now());
    const id = window.setInterval(() => setNow(Date.now()), 1000);
    return () => window.clearInterval(id);
  }, []);

  return (
    <time dateTime={new Date(now).toISOString()}>
      {new Date(now).toLocaleTimeString()}
    </time>
  );
}

// Useful escape hatch when the value is fundamentally unknowable on the
// server (geolocation, viewport, user prefs from localStorage).
export function ClientOnly({
  fallback,
  children,
}: {
  fallback: React.ReactNode;
  children: React.ReactNode;
}) {
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return <>{mounted ? children : fallback}</>;
}

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Reproduce
Render the broken Date.now-in-render version twice with renderToString; assert the outputs differ. The hydration warning in the browser is the same root cause.
Patch
Pass server time as a prop. Use useEffect for the live upgrade. Test SSR determinism explicitly. For client-only fields, use the ClientOnly gate with a server-safe fallback.
Prevent
Add an ESLint rule that flags Date.now/Math.random/window in JSX expressions of components rendered on the server. Pair with a CI test that asserts no console warnings during hydrateRoot in jsdom.
import { describe, it, expect } from 'vitest';
import { renderToString } from 'react-dom/server';

describe('Clock hydration regression', () => {
  it('produces deterministic SSR output for the same props', () => {
    const html1 = renderToString(<Clock serverNow={1700000000000} />);
    const html2 = renderToString(<Clock serverNow={1700000000000} />);
    // The broken version called Date.now() during render; back-to-back
    // renders produced different output and hydration always warned.
    expect(html1).toBe(html2);
  });

  it('ClientOnly emits the fallback on the server', () => {
    const html = renderToString(
      <ClientOnly fallback={<span>Loading</span>}>
        <span>Live</span>
      </ClientOnly>
    );
    expect(html).toContain('Loading');
    expect(html).not.toContain('Live');
  });
});

Interviewer Signal

Tests whether you debug from ownership and lifecycle instead of random dependency-array edits.

Constraints

  • State a hypothesis before changing code.
  • Name what evidence would confirm the bug.
  • Avoid broad rewrites unless the current API cannot express the behavior.

Model Answer Shape

  • Reproduce the failing sequence first.
  • Inspect ownership boundaries: local state, props, effects, subscriptions, and server data.
  • Patch the minimal broken boundary and add a regression test.

Tradeoffs

  • A minimal patch reduces risk, but repeated lifecycle bugs often justify a small reducer or custom hook.
  • Adding dependencies can silence lint warnings while still preserving the wrong ownership model.

Edge Cases

  • Double clicks and repeated submissions.
  • Slow network responses arriving out of order.
  • Component remount with stale persisted state.

Testing And Proof

  • Failing interaction sequence.
  • Out-of-order async response.
  • Unmount cleanup.

Follow-Ups

  • What would the code review comment say?
  • What metric or log would show this in production?