Hydration Proof

Search documentation

Find a page or section

HP1012: Form state differs between server and client

HP1012 (form state mismatch): a form control's value, checked or selected state changed during hydration, and React never reports it. Causes and the fix.

HP1012 (form-state-mismatch) means a form control's value, checked state or selected option in the server HTML differs from what React renders on the client. React sets these as properties during hydration, so the field changes under the user, and React reports nothing, not even in development. Use defaultValue and defaultChecked with values that are identical on both sides.

CodeHP1012
Nameform-state-mismatch
Default severityWarning
GroupDOM mismatches
What it meansA form control value, checked or selected state changed during hydration.

What HP1012 (form-state-mismatch) means

On the server, React writes a control's state into the HTML: the value attribute of an <input>, the text of a <textarea>, the checked attribute, and selected on the chosen <option>. In the browser, React applies the client value as a DOM property. When the two differ, the value the page showed is replaced during hydration.

hydration-proof reads each control's state from the server HTML the way the browser's parser does and compares it with the value, defaultValue, checked or defaultChecked React renders. The package's test pages confirm that React 18 and 19 are silent here: a <textarea> whose defaultValue differs and a <select> whose selected option differs produce no React message at all.

Text a user types before hydration and loses is a different problem, HP5002.

Likely causes

  • A default read from browser storage or behind a typeof window check: a browser-only API.
  • A form the server rendered with the result of a submitted Server Action (useActionState with a permalink) that the client hydrated without that state: Server Action form state.
  • A date or number default formatted with the runtime's locale.

How to fix it

  • Use defaultValue/defaultChecked with values that are identical on server and client.

A currency picker that restores the saved choice during render:

currency-picker.tsx
"use client";
 
export function CurrencyPicker() {
  const saved =
    typeof window === "undefined"
      ? "EUR"
      : (localStorage.getItem("currency") ?? "EUR");
 
  return (
    <select id="currency" defaultValue={saved} aria-label="Currency">
      <option value="EUR">Euro</option>
      <option value="USD">US dollar</option>
    </select>
  );
}

Let the server decide the default (from a cookie, the user's profile or the URL) and pass it in, so both renders select the same option:

currency-picker.tsx
export function CurrencyPicker({ currency }: { currency: string }) {
  return (
    <select id="currency" defaultValue={currency} aria-label="Currency">
      <option value="EUR">Euro</option>
      <option value="USD">US dollar</option>
    </select>
  );
}

For Server Action state, let the framework pass the form state to hydrateRoot (Next.js does this) and render the form from the action state only.

Prevent it with ESLint

no-storage-in-initial-render, no-client-only-initial-state and no-window-render-branch report the browser reads that make a default differ.

Example

  ⚠ /checkout 1.2s  1 warning
    HP1012 Form state differs between server and client  (localstorage / sessionstorage read during render, 84%)
      #currency  in CurrencyPicker
      attribute: value
      server: "EUR"
      client: "USD"
      components/currency-picker.tsx:10:5
      → Storage is only available in the browser. Render a neutral value first and read storage in useEffect.