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.
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?