# 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.

Source: https://hydration.jscrate.dev/docs/issues/hp1012
Last updated: 2026-09-18

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](https://hydration.jscrate.dev/docs/issues/hp5002).

## Likely causes

- A default read from [browser storage](https://hydration.jscrate.dev/docs/causes/storage) or behind a
  `typeof window` check: a [browser-only API](https://hydration.jscrate.dev/docs/causes/browser-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](https://hydration.jscrate.dev/docs/causes/form-state).
- A date or number default formatted with the runtime's
  [locale](https://hydration.jscrate.dev/docs/causes/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:

```tsx title="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:

```tsx title="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`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render),
[`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state) and
[`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) report the
browser reads that make a default differ.

## Example

```text
  ⚠ /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.
```

## Related

- [HP5002: user input was reset during hydration](https://hydration.jscrate.dev/docs/issues/hp5002)
- [Server Action form state](https://hydration.jscrate.dev/docs/causes/form-state)
- [HP1013: dangerouslySetInnerHTML differs](https://hydration.jscrate.dev/docs/issues/hp1013)
- [HP1001: text differs](https://hydration.jscrate.dev/docs/issues/hp1001)
