cswork / libraryOpen practice workspace

Articles

React Hydration Interview Questions: Mismatches, Browser State, and Recovery

Diagnose React hydration mismatches with initial-render timelines, browser preferences, stable snapshots, and recovery checks that verify behavior.

The server renders “Compact view.” The browser's first React render reads a saved preference and produces “Expanded view.” Both branches work independently, and a typeof window guard prevents a server crash. The page still has a hydration problem because its two initial outputs disagree.

React hydration interview questions become easier when you separate three moments: the HTML produced by the server, the DOM the browser has parsed before React takes over, and the first client render. Later updates are a different part of the timeline.

Evidence boundary: React and Next.js documentation establish the behavior discussed below. The preference panel, investigation matrix, and test scenarios are original exercises, not candidate reports or claims about an employer's interview. Practice the diagnosis with Implement and Improve React Components.

What must match during hydration?

Official React contract: hydrateRoot attaches React to previously generated HTML and expects the initial client output to match. React treats mismatches as bugs; it does not guarantee that mismatched attributes will be repaired. Frameworks often own the root call.

In the preference example, the relevant comparison is not the final screen after all effects have run. It is the first client output against the existing server-rendered content. If those differ, a later state update does not retroactively make the initial match correct.

Distinguish three artifacts while debugging. The network response contains the server's HTML. The browser parser turns that HTML into DOM nodes and may repair invalid structure. React then evaluates the client tree against that DOM. A screenshot of the final page hides the order in which those steps happened.

Ask whether the application is server-rendered at all. A component mounted into an empty client-only root has no server snapshot to hydrate. A bug after ordinary client navigation may resemble a hydration symptom but originate in state, routing, or effects instead.

Also record the React version, framework version, route, and whether the failure follows a hard refresh. Those facts make the reproduction more useful than “React sometimes shows a warning.”

Why does checking for window not solve the mismatch?

Consider this original example:

function PreferenceLabel() {
  const mode = typeof window === "undefined"
    ? "compact"
    : localStorage.getItem("mode") ?? "compact";

  return <p>{mode === "expanded" ? "Expanded view" : "Compact view"}</p>;
}

The guard answers an availability question: can this environment access a browser API? It does not answer an equality question: will both environments produce the same first output?

With mode=expanded in browser storage, the server produces one label and the client produces another. With empty storage, the bug may disappear. That is why a developer testing only a clean browser can miss a returning-user failure.

A lazy state initializer does not automatically fix it. Reading storage inside useState(() => ...) can still make the initial client state differ from the server's initial state. Moving code into a different function is not the same as moving it to a later phase.

The practical diagnostic step is to enumerate the input values for each environment. List the server fallback, serialized props, cookies available to the request, and browser storage. Identify the first input that diverges and the exact text or attribute it changes.

How can a saved browser preference initialize safely?

For this exercise, assume the server cannot know the saved preference and a neutral compact layout is acceptable initially. Begin with the same state in both environments, then read storage after the client commits.

import { useEffect, useState } from "react";

function PreferenceLabel() {
  const [mode, setMode] = useState("compact");

  useEffect(() => {
    try {
      const saved = localStorage.getItem("mode");
      if (saved === "compact" || saved === "expanded") {
        setMode(saved);
      }
    } catch {
      // Keep the usable default if storage is unavailable.
    }
  }, []);

  return <p>{mode === "expanded" ? "Expanded view" : "Compact view"}</p>;
}

Official lifecycle fact: React's useEffect reference states that effects run only on the client. Here the initial state is deliberately shared; the effect provides a later update. The code validates the saved value and keeps a usable default if the storage read fails.

The expected timeline is concrete:

MomentInput usedRendered label
Server renderExplicit compact defaultCompact view
First client renderThe same compact defaultCompact view
Client effect reads expanded preferenceValid saved browser valueExpanded view

This policy has a visible cost: a returning user may briefly see the default. Reserve layout space and consider whether that change is acceptable. If the product requires the correct preference in the initial HTML, make it available through a suitable request-time input and send the same initial value to the client.

For example, a preference cookie can inform server rendering, but it introduces caching and synchronization decisions. A shared cache must not serve one user's personalized representation to another. Storage and cookie disagreement also needs a defined precedence rule.

Do not move the whole page behind a “mounted” flag without considering what users receive before JavaScript loads. A narrowly scoped preference adjustment may be preferable to withholding the useful content.

Which mismatch causes should you investigate first?

Official framework guidance: the Next.js hydration-error documentation identifies causes including invalid HTML nesting, browser-only rendering logic, time-dependent values, extensions that modify HTML, and incorrectly configured HTML transformations. Treat these as hypotheses to test, not a reason to blame every layer at once.

Use the smallest visible mismatch to guide the investigation:

SymptomEvidence to compareFocused experiment
Different date textTimestamp, locale, and time zone on both sidesSupply one fixed instant and explicit formatting inputs
Different selected optionSerialized default versus browser preferenceReproduce with empty and populated storage
Unexpected DOM nestingResponse HTML versus parsed DOMReduce to the invalid parent-child pair
Mismatch only after deploymentHTML and JavaScript build identitiesConfirm the response and assets belong together
Failure in one browser profileDOM changes before hydrationRepeat in a controlled profile without extensions
Different generated IDsTree structure and root configurationCompare the server and client component paths

Change one variable at a time. If you simultaneously clear storage, disable server rendering, and replace the component, a passing page will not tell you which cause mattered.

A deployment-only problem also deserves an artifact check. A cached HTML document from one build and client assets from another can disagree even when each build was internally consistent. Capture the document and asset identities before a reload discards the evidence.

Avoid dumping personalized HTML or storage contents into unrestricted logs. Preserve the differing field, sanitized context, and build identifiers needed to reproduce the issue.

How do dates, randomness, and IDs fit the same model?

The general question is whether rendering depends on an input that is stable across the two environments. Calling the clock during each render can produce different instants. Formatting the same instant can still differ when locale or time zone differs. Random values introduce another independent input.

For an original appointment example, choose an ISO timestamp and a specified display zone. Pass the timestamp and formatting policy with the initial data. If the interface intentionally switches to the user's local zone after hydration, model that as a later update with an explicit initial representation.

Do not fix this by hardcoding the current date in the component. The goal is one shared input for the initial render, not a stale output that happens to stop today's warning.

Official ID guidance: React's useId documentation supports IDs used for accessibility relationships and requires matching server/client trees for hydration. It is not a generator for list keys. Multiple-root configuration also needs consistent identity handling.

In an interview, separate a label/input association from a data item's identity. Stable data keys and generated accessibility IDs solve different problems. Replacing every identifier with an incrementing module-level counter does not establish consistent rendering across requests and environments.

When is suppressHydrationWarning appropriate?

Official escape-hatch behavior: React documents suppressHydrationWarning for unavoidable differences on a particular element. It works one level deep and does not make React patch mismatched text. It should not become a blanket treatment for an unexplained subtree. See the hydrateRoot reference.

Before using it, identify the exact intentional difference and explain how the correct value will eventually be shown. A display-only timestamp with a deliberate update policy is different from a price, permission-dependent control, or selected form value that must be correct immediately.

Silencing a message is not evidence that the DOM, attributes, and interaction behavior agree. Inspect the displayed value and exercise the associated control. A button appearing in the right position does not prove it has the intended semantics after recovery.

Disabling server rendering for a small browser-dependent widget can also be a legitimate product decision. Its cost is a different initial experience and dependence on client JavaScript. Explain that trade-off and keep the change scoped to the component that actually needs it.

What should you say about external stores?

A shared external store can change between server rendering and client startup. Reading its current browser value without coordinating the hydration snapshot can reproduce the same mismatch as local storage.

Official API behavior: useSyncExternalStore accepts a getServerSnapshot function for server rendering and hydration. Its initial snapshot must agree between environments. Snapshot caching and subscription behavior matter too; returning a freshly allocated object on every read is not a sound store contract.

For a practice exercise, define a small store with a serialized initial count. Use that same count during hydration, then allow the current browser snapshot to drive later updates. Test a store change that occurs before the component hydrates rather than assuming the store remains frozen throughout startup.

Do not describe the hook as automatically synchronizing arbitrary server and browser data. You still choose the snapshot and transfer mechanism. The hook provides an integration contract; it cannot infer which server value the browser should trust.

How do you verify recovery without declaring the bug fixed?

Recovery means the application found a way to continue. It does not prove the original mismatch was harmless. Record the framework's supported recovery signal and component context, then compare behavior before and after the repair.

At an application-owned root, React exposes onRecoverableError; in a framework, use the supported integration rather than creating a second hydration root. Keep telemetry free of secrets and correlate failures with route and release information. The root API reference documents the callback.

The regression test should reproduce the original differing input: populated storage, a specified locale, an altered DOM fixture, or mismatched initial data. Check the initial representation, the later expected update, and the relevant user interaction. Merely asserting that the final text eventually appears can miss an initial mismatch and recovery.

Test hard navigation as well as client navigation. Include a slower JavaScript load so the initial server output remains visible long enough to inspect. If an input is present during startup, test whether user-entered data survives the scenario. Do not promise identical recovery scope across every React and framework version; observe the installed application.

Sources and Further Reading