What is hydration in Next.js and when can it cause UI mismatches
Hydration: the client runs React on the same component tree the server already rendered, attaching event handlers and re-running effects, but **reusing existing HTML**. Mismatches happen when server and client render different output — caused by time/dates, browser-only APIs (`window`), random values, feature flags differing, or `useEffect`-style state read during render. Use `useId`, guard browser globals, serialize server data, or render client-only via dynamic imports.
Hydration is React's process of turning server-rendered HTML into a live, interactive app. In Next.js (and any SSR framework), it's where the most subtle bugs hide — the page looks right but secretly mismatched.
What hydration actually does
- Server renders the tree → HTML in the response.
- Browser loads the page, parses HTML — instant content.
- JS bundle loads, React boots, runs the same component tree.
- React walks the existing DOM and attaches event handlers and refs without recreating nodes.
- If client output matches server, hydration succeeds; if not, warning + possible subtree re-render.
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);When mismatches happen
A mismatch means server-rendered HTML doesn't equal what the client would render right now. Common causes:
1. Time / dates
<p>{new Date().toLocaleString()}</p>Server time is when the request was rendered; client time is when hydration runs. They differ by ms to seconds.
Fix: format with a stable locale + timezone on the server, or render placeholder server-side and update in useEffect.
2. Browser-only APIs
<div>{window.innerWidth > 768 ? "desktop" : "mobile"}</div>window doesn't exist on the server. Server crashes or returns "mobile" (default); client returns whatever it sees.
Fix:
const [width, setWidth] = useState(null);
useEffect(() => { setWidth(window.innerWidth); }, []);
return <div>{width && (width > 768 ? "desktop" : "mobile")}</div>;Or in Next.js, dynamic import with ssr: false for fully client-only components.
3. Random values
const id = useMemo(() => crypto.randomUUID(), []);Different on server vs client.
Fix: useId() — designed to be stable across SSR/CSR.
4. Feature flags / experiments
If the flag source differs between server and client:
Server: flag from cookie at request time → A.
Client: flag from a fetched config → B.Fix: Pass the server-determined value through props or as serialized state. Client uses the same value initially; can re-evaluate after mount if needed.
5. Locale / user agent
Server detects "en-US" but client navigator says "en-GB" → number/date formats differ.
Fix: server passes the locale used for rendering; client reads from the prop, not its own detection, on first render.
6. Authentication state
If the server renders "Hi user" with auth cookies but the client doesn't have access (or vice versa), the greeting differs.
Fix: render the authenticated subtree only when both server and client agree on auth.
What Next.js does
useId()for stable ids.- Automatic hydration matching warnings in dev mode.
dynamic(() => import(...), { ssr: false })for client-only components.- App Router + RSC sidesteps hydration entirely for server components.
Why mismatches are dangerous
- Console warning in dev.
- React 18 may discard the server HTML for the mismatched subtree and re-render client-only — losing the SSR FCP benefit.
- Invisible bugs: visual UI looks fine but state is subtly wrong (different feature flag rendered, etc.).
- A11y / SEO regressions if the server-rendered version was correct and client overrides.
Detecting mismatches in production
Capture them in your error logger:
const original = console.error;
console.error = (...args) => {
if (args[0]?.includes?.("Hydration")) Sentry.captureMessage("Hydration mismatch", { extra: args });
original(...args);
};Patterns to avoid
typeof window !== "undefined"in render — splits server/client render → mismatch.- Conditional rendering on
isClientthat flips on mount — causes a flash. - Suppressing hydration warnings when the cause is fixable — pretends the bug doesn't exist.
Interview framing
"Hydration is the client running React on a tree the server already rendered, attaching handlers to existing HTML without recreating it. The risk is mismatches — when server and client render different output. Causes: time/dates, browser-only APIs (window, navigator) used in render, random values without useId, feature flags or locale or auth state evaluated differently on server vs client. Fixes: use useId for stable ids, format on the server with a stable locale, guard browser globals to useEffect-only, pass server-determined flags/locale/auth as props so client uses the same values on first render. For genuinely client-only components, Next.js's dynamic(..., { ssr: false }) or RSC client boundary. The danger isn't just the warning — React 18 may discard server HTML on mismatch, defeating the SSR win, and the bugs are often invisible (different flag rendered) rather than visible."
Follow-up questions
- •Why does React 18 discard the server HTML on mismatch?
- •What does `useId` do and why is it the right fix?
- •When should you use `dynamic({ ssr: false })`?
- •How do you detect hydration mismatches in production?
Common mistakes
- •`typeof window` checks in render.
- •Math.random / Date.now in render.
- •Suppressing warnings without fixing.
- •Feature flag source differing server vs client.
Performance considerations
- •Mismatches lose the SSR FCP win for the affected subtree. Hydration itself is a long task — see [[how-do-you-handle-ssr-hydration-in-complex-apps]].
Edge cases
- •Hydration after navigation (not initial load).
- •Hydrating into a different layout from the route's actual render.
- •Streamed Suspense boundaries with their own hydration.
Real-world examples
- •Next.js, Remix, Gatsby — all share the hydration model.