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.
| Code | HP1012 |
|---|---|
| Name | form-state-mismatch |
| Default severity | Warning |
| Group | DOM mismatches |
| What it means | A 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 windowcheck: a browser-only API. - A form the server rendered with the result of a submitted Server Action
(
useActionStatewith 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/defaultCheckedwith values that are identical on server and client.
A currency picker that restores the saved choice during render:
"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:
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.