Implement hydration mismatch debugging in a product component
Use hydration mismatch debugging to solve a realistic React workflow. Keep rendering, user intent, async synchronization, and error states separate.
Answer Strategy
Hydration mismatch is the question that asks whether you understand the contract between server and client renders. The rule: the first client render must produce the exact same React tree as the server render. Anything that differs — Date.now, window.innerWidth, Math.random, localStorage, user-agent branches — must be deferred until after hydration commits, otherwise React patches over the difference and you get inconsistent DOM, lost event handlers, or visible flicker.
Two patterns cover most cases. The "server-injected initial value" pattern (used by the Clock above): the server passes the timestamp it rendered with as a prop, the client renders the same value on first paint, then a useEffect upgrades to the live one. The "ClientOnly" gate pattern: render a fallback during SSR and the first client render, flip to the real children after the mount effect. Use the first when you can; use the second when the value is fundamentally unknown server-side.
Adjacent traps: reading typeof window during render (still mismatches because the server tree is generated without window, but the client first render also lacks the post-mount flag), wrapping the whole page in suppressHydrationWarning (hides every problem, not just the one you understand), and using useId incorrectly (useId is the right answer for a11y ids that need SSR stability — do not generate ids with Math.random). The Clock pattern is the canonical reference; ClientOnly is the escape hatch.
Reference Implementation: Server-Safe Clock With Effect Upgrade
A Clock component that renders the server-injected timestamp first and upgrades to the live value inside useEffect after hydration commits.
// Hydration mismatch happens when the server-rendered HTML disagrees with
// the client’s first render. The canonical cause is reading a value that
// only exists on the client (Date.now, window, navigator, localStorage)
// during render. The fix is to render the server-safe value first and
// promote to the client value inside useEffect — the SSR snapshot stays
// stable, and the client upgrade happens after hydration commits.
type ClockProps = {
serverNow: number; // injected by the server route handler
};
export function Clock({ serverNow }: ClockProps) {
// Render the server-known value on first paint (server and client agree)
// — guaranteed not to mismatch.
const [now, setNow] = React.useState(serverNow);
// useEffect only runs on the client; updating state here causes a second
// render with the live value. Hydration is already complete so React
// does not warn.
React.useEffect(() => {
setNow(Date.now());
const id = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(id);
}, []);
return (
<time suppressHydrationWarning dateTime={new Date(now).toISOString()}>
{new Date(now).toLocaleTimeString()}
</time>
);
}
// Useful pattern for ANY client-only render: the gate. Safer than reading
// window during render because typeof window === 'undefined' check still
// runs on first client render and could return a different result than
// the server.
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', () => {
it('server markup is deterministic across renders with the same props', () => {
const serverNow = 1700000000000;
const first = renderToString(<Clock serverNow={serverNow} />);
const second = renderToString(<Clock serverNow={serverNow} />);
// Hydration only succeeds when the server output is byte-for-byte
// stable. If we read Date.now() during render, this assertion fails.
expect(first).toBe(second);
expect(first).toContain('time');
});
it('ClientOnly returns the fallback during server rendering', () => {
const html = renderToString(
<ClientOnly fallback={<span>Loading</span>}>
<span>Live</span>
</ClientOnly>
);
// useEffect did not run yet — the server emitted the fallback so
// hydration sees the same tree, then the post-mount setMounted(true)
// upgrades to the children without a mismatch warning.
expect(html).toContain('Loading');
expect(html).not.toContain('Live');
});
});Interviewer Signal
Shows whether you can explain React behavior while building maintainable product UI.
Constraints
- Name what is render-derived and what is stored state.
- Keep side effects owned by events or effects deliberately.
- Provide a testable boundary for business logic.
Model Answer Shape
- Start from the user workflow and state ownership.
- Move pure decisions out of JSX when the branch logic grows.
- Use React primitives to express ownership, not to hide unclear state.
Tradeoffs
- Colocating state improves clarity until sibling coordination becomes the real problem.
- Memoization helps only after render cost or identity churn is measured.
Edge Cases
- Strict Mode re-running development effects.
- Stale closures after async work resolves.
- Unmounts and route changes during in-flight operations.
Testing And Proof
- Reducer or pure function test for core state transitions.
- Interaction test for the user workflow.
- Regression case for stale or repeated async behavior.
Follow-Ups
- How would this change with server rendering?
- Where would you place this state in a larger app?