A server-rendered React app chooses its theme by reading localStorage during render. In the browser it briefly shows the light theme and logs a hydration mismatch. Explain the cause and the options you would consider for fixing it.
answer
- the server cannot read the browser
- hydration needs agreement, not correctness
- suppressing the warning leaves the flash
- cookie is readable by the server
- apply it before first paint
basics
~20 slocalStorage does not exist on the server, so the server rendered a default theme while the browser's first render read the stored one — two different trees. Fix it by rendering a server-known default and applying the stored theme after hydration, or by setting it before hydration outside React's tree.
solid answer
~60 sThe server has no `localStorage`, no `window`, and no access to a client-only preference, so it renders whatever default your code falls back to. The browser's first render reads the real value and produces a different tree, which is exactly what a hydration mismatch is. There are three honest options. Render the server default and correct it in an effect after mount — simple, but the user sees a flash. Read the preference through `useSyncExternalStore`, whose server-snapshot value is also used during hydration, so React renders the deterministic default first and the subscribed value takes over afterwards — same flash, but the subscription keeps the value consistent. Or move the decision out of React's render entirely: a small blocking script before hydration sets a `data-theme` attribute or class on the document element, CSS keys off it so there is no flash, and React never renders that attribute — with `suppressHydrationWarning` on the element if React does own it. Storing the preference in a cookie is the fourth option, because then the server can know it.
go deeper
Know that the server has no window, document or localStorage, so anything read from the browser during render will disagree with the server's output.
Explain the two-pass fix — render the server-safe default, correct in an effect — and why effects are safe while render-time reads are not.
Compare the real options and their costs: post-mount correction (flash), a pre-hydration script (no flash, blocking script), a cookie (server-known), and useSyncExternalStore with a deterministic server snapshot.
Own the decision upstream: choose a storage mechanism the server can read for anything affecting above-the-fold rendering, prefer CSS for what CSS can express, and set a rule that render never branches on environment detection.
## Why this is the most instructive mismatch Time and randomness mismatches are accidents. This one is architectural: the value genuinely lives only in the browser, so no amount of care makes the server's first render correct. The interviewer is checking whether you understand that hydration requires *agreement*, not correctness — and that you know the fix is to choose what both sides will agree on. ## What actually happens On the server, `typeof window === 'undefined'`. Code like `localStorage.getItem('theme')` either throws or, more commonly, sits behind a guard that falls back to `'light'`. The HTML ships with the light theme. In the browser the same component runs again for hydration, this time reading `'dark'` from storage, and renders a different class. React finds the disagreement, reports a hydration error, and client-renders that subtree. The user sees light, then dark — and the flash is visible even when the mismatch is later suppressed, because the *server HTML painted first*. Silencing the warning does not remove the flash; only rendering the right thing before paint does. ## Option 1 — deterministic first render, correct after mount ```jsx function useTheme() { const [theme, setTheme] = useState('light'); // matches the server useEffect(() => { const stored = localStorage.getItem('theme'); if (stored) setTheme(stored); }, []); return theme; } ``` Correct by construction, because effects never run on the server and run after hydration commits on the client. The cost is one flash of the wrong theme. Acceptable for a small widget; poor for a full-page theme. ## Option 2 — useSyncExternalStore with a server snapshot `useSyncExternalStore` takes a subscribe function, a client snapshot getter, and a server snapshot getter. The third one is used during server rendering *and* during the client's hydration render, which is precisely the property that prevents mismatches: both runs read the same deterministic value. After hydration, React switches to the client snapshot and the subscription keeps it in sync. The discipline this imposes is the useful part — the server snapshot must return a value the server could actually have produced. If you cheat and read `localStorage` from it, you are back to a mismatch. The flash remains; what you gain is a single, consistent way to read a browser-only source across the app, one that also handles updates from other tabs. ## Option 3 — decide before React renders The only way to avoid the flash is to have the correct value applied before first paint. A tiny synchronous script in the document head reads storage and sets an attribute: ```html <script> document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light'; </script> ``` CSS keys off `[data-theme='dark']`, so the very first paint is correct. React never renders that attribute, so there is nothing to mismatch. When React *does* own the element that the script mutates, put `suppressHydrationWarning` on it — this is the textbook legitimate use, because the difference is intentional and understood. Note the tradeoff: a blocking inline script costs a little render-blocking time, and it must be idempotent with whatever React later renders. ## Option 4 — make it server-known If the preference lives in a cookie rather than `localStorage`, the server receives it with the request, renders the right theme, and both runs agree with no flash and no special hooks. This is usually the best answer for anything that affects above-the-fold rendering — the real problem was choosing a storage mechanism the server cannot see. ## The general rule The same analysis covers every browser-only read during render: `matchMedia` for a media query or `prefers-color-scheme`, `navigator.userAgent` for device branching, `window.innerWidth` for a responsive layout, feature detection. Each of them makes the client's first render depend on something the server cannot know. So the reflexes worth naming: - Never branch a render on `typeof window` to produce different output — that guarantees a mismatch. - Prefer CSS for anything CSS can express: media queries and `prefers-color-scheme` need no JavaScript and no server knowledge, so they never mismatch. - If the value must come from JavaScript, decide whether it can move to a cookie (server-known), be applied before hydration (no flash), or be corrected after mount (flash, simplest). A candidate who lists those three destinations — server-known, pre-hydration, post-hydration — has answered the question, whatever they pick.
- Why does silencing the mismatch not remove the flash of the wrong theme?Because the flash comes from the server HTML painting before any JavaScript runs. Suppressing the warning only affects what React reports during hydration; the browser has already shown the server's default. To remove the flash, the correct value has to be present before first paint — from a cookie the server can read, or from a synchronous script that runs before hydration.
- What must the server-snapshot function passed to useSyncExternalStore return, and why does that matter here?It must return a value the server can compute deterministically — a constant default, not a read of localStorage. React uses it both for server rendering and for the client's hydration render, so returning anything browser-specific reintroduces the exact mismatch it exists to prevent. The real browser value arrives from the client snapshot after hydration.
- A component renders a mobile layout when window.innerWidth is under 768px. Why is that a hydration bug, and what would you do instead?The server has no window, so it renders one branch and the browser's first render may render the other. Express the breakpoint in CSS media queries where possible, so no JavaScript decides it and nothing can disagree. If you genuinely need the measurement in JavaScript, render the server-safe layout and switch after mount, accepting the reflow.
saying these in an interview costs you the question
- Says to read localStorage inside useSyncExternalStore's server snapshot
- Treats suppressHydrationWarning as a fix for the theme flash
- Branches on typeof window during render to pick the output
- Claims localStorage is available on the server via a polyfill in Node
- Says the mismatch is harmless because the client value wins anyway