“Your server-rendered page logs a hydration mismatch. What does that mean, and how do you fix it without just hiding the warning?” Anyone who has worked with Next.js, Remix or a custom SSR setup has seen this error. Interviewers ask it to check whether you understand what hydration does and why the first client render is special.
A strong answer names the typical causes from memory and explains the correct fix for each, rather than reaching for suppressHydrationWarning or turning off server rendering.
Before you start
You should know the difference between server rendering (generating HTML on the server) and client rendering, and that the browser receives HTML first and JavaScript afterwards. You should know useEffect and useState. React 18 introduced hydrateRoot and selective hydration with Suspense; React 19 improved hydration error messages to show a diff of the mismatched content and became more tolerant of third-party scripts and browser extensions that insert tags into <head> and <body>.
The short answer
Hydration is React adopting server-rendered HTML: instead of creating DOM nodes, it walks the existing ones, attaches event handlers, and expects its first client render to produce exactly the same markup. A mismatch happens when render output depends on something that differs between server and client, such as the current time, Math.random(), window or localStorage, the user’s locale or time zone, or invalid HTML that the browser’s parser rearranged. Fix it by making the first client render identical to the server output, then updating client-only values in an effect after hydration, and by using useId for generated ids.
How it works
The server runs your components and sends HTML. The browser displays it immediately. When the JavaScript loads, hydrateRoot(container, <App />) renders the same tree on the client and matches it, node by node, against the existing DOM.
function Greeting() {
const hour = new Date().getHours();
return <p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>;
}If the server runs in UTC and the user is in Tokyo, the server might say “Good morning” while the browser computes “Good afternoon”. React finds different text. In development it reports the mismatch; for text differences React 19 tells you which content differed. When it cannot reconcile the difference, React throws away the server HTML for the nearest Suspense boundary (or the whole root) and renders it again on the client. That means a visual flash, lost performance and possibly lost focus or scroll position.
Hydration also attaches events only after it reaches a component, so mismatches tend to appear exactly where you most need the page to be interactive quickly.
Step-by-step walkthrough
Step 1: Read the error and find the component
React 19 prints the server and client values with a diff. Search for what produced that value. Common culprits:
<span>{Date.now()}</span> // time
<div id={`field-${Math.random()}`} /> // randomness
{typeof window !== 'undefined' && <Banner />} // environment branching
<p>{price.toLocaleString()}</p> // locale differences
<p><div>nested block</div></p> // invalid HTML nestingStep 2: Use useId for ids
useId generates ids from the component’s position in the tree, so server and client agree:
function EmailField() {
const id = useId();
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);
}Do not use it for list keys; keys should come from data.
Step 3: Defer client-only values until after hydration
Render the server-safe version first, then switch in an effect, which runs only in the browser after hydration:
function LocalTime({ iso }) {
const [text, setText] = useState(null);
useEffect(() => {
setText(new Date(iso).toLocaleTimeString());
}, [iso]);
return <time dateTime={iso}>{text ?? iso.slice(11, 16) + ' UTC'}</time>;
}Server and first client render both output the UTC fallback, so hydration matches. The local time appears right after. For a value from an external source like localStorage or a media query, useSyncExternalStore with a getServerSnapshot gives the same guarantee; see external store subscriptions.
Step 4: Fix invalid HTML
The browser’s parser repairs invalid nesting before React sees the DOM. A <div> inside a <p>, an <a> inside an <a>, or a <tr> directly inside <table> without <tbody> becomes a different DOM tree than React expects. Correct the markup; no runtime trick will fix this.
Worked scenario
An e-commerce site showed “Free shipping ends in 3h 12m” using a countdown computed in render. On product pages, React logged mismatches and the whole buy-box re-rendered on the client, occasionally wiping a quantity the user had already changed while the page was still loading. Lighthouse also showed worse interaction timing on those pages.
The countdown read Date.now() during render. The server’s value was seconds or minutes older than the client’s by the time the bundle loaded.
The fix rendered a stable placeholder on both sides and started the countdown after hydration:
function Countdown({ endsAt }) {
const [remaining, setRemaining] = useState(null);
useEffect(() => {
const tick = () => setRemaining(Math.max(0, endsAt - Date.now()));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [endsAt]);
if (remaining === null) return <span>Free shipping today</span>;
return <span>Free shipping ends in {format(remaining)}</span>;
}The buy-box now hydrates cleanly, and the countdown appears a moment later without disturbing anything around it. Wrapping slow, non-critical parts of the page in <Suspense> boundaries also let React hydrate the buy-box first.
Common mistake
- Checking
typeof windowin render. The server renders one branch, the client another, and that mismatch is guaranteed. - Blanket
suppressHydrationWarning. It only works one level deep on a single element’s text or attributes, and it hides real bugs. It is appropriate for a timestamp you deliberately let differ. - Disabling SSR for the whole page. It removes the symptom and the benefits of server rendering.
- Generating ids with a counter. Module-level counters drift between server and client and across requests. Use
useId. - Blaming React for extension-injected markup. React 19 tolerates many unexpected tags from extensions and third-party scripts in
<head>and<body>, but it cannot fix mismatches in your own components.
Verify the behavior
Render a component on the server and hydrate it in a test environment to catch mismatches automatically:
import { renderToString } from 'react-dom/server';
import { hydrateRoot } from 'react-dom/client';
const container = document.createElement('div');
container.innerHTML = renderToString(<App />);
document.body.appendChild(container);
const errors = [];
hydrateRoot(container, <App />, {
onRecoverableError: error => errors.push(error),
});
// after React has finished hydrating, expect(errors).toEqual([])onRecoverableError receives hydration mismatches in both React 18 and 19. Running this in a jsdom test (using act to flush) with the clock or locale mocked differently on each side reproduces time-zone bugs before they reach production. In the browser, the development console shows the diff directly.
Follow-up questions
What is selective hydration? With Suspense boundaries, React 18+ hydrates parts of the page independently and prioritises the part the user interacts with. See Suspense boundaries.
Can Server Components cause mismatches? Their output is already HTML and serialised data, so they do not hydrate themselves; mismatches come from the Client Components they render. See Server Components boundaries.
Why not render client-only components with useEffect and a mounted flag everywhere? It works but shifts all that content to after hydration, hurting performance and causing layout shifts. Use it only for genuinely client-dependent pieces.
Does a mismatch break the page in production? React recovers by client-rendering the affected boundary, so the page works, but you lose the SSR benefit for that subtree and may see a flash.
Interview exercise
A header shows “Sign in” on the server and the user’s name on the client, read from localStorage in render. Hydration fails. Give the immediate fix and the better architectural fix.
Answer and reasoning
The immediate fix is to make the first client render match the server: render “Sign in” (or a neutral placeholder of the same size) initially, and read localStorage in an effect or through useSyncExternalStore with a server snapshot of null, then show the name after hydration. The architectural fix is to stop storing the session only in the browser: put it in an HTTP-only cookie so the server knows who the user is and renders their name directly. Then server and client agree from the start, there is no flash from “Sign in” to the name, and the token is not exposed to scripts.
Continue learning
Practise with the React interview questions and the React MCQs. Related notes: Suspense boundaries, Server Components boundaries and external store subscriptions. Official reference: the hydrateRoot reference on react.dev.