Hydration Proof

Search documentation

Find a page or section

Fix Server Action form state hydration

The server remembered the submission. The client did not.

Server Action form state hydration breaks when the server renders a submitted form, but the browser starts with the form's initial state. Fields or messages can reset as the page becomes interactive. Make sure the action result reaches the browser and that both renders use it.

Symptoms

React may not warn about a changed form value or selected option. The symptom is often a field that resets or a submitted message that disappears.

hydration-proof reports it as HP1012, a warning:

HP1012 Form state differs between server and client  (server action form state, 90%)
  #newsletter-email
  attribute: value
  server: "ada@example.com"
  client: ""
  → The server rendered this form with the result of a submitted Server Action (useActionState with a permalink), but the client hydrated without that state. Let the framework pass the form state to hydrateRoot (Next.js does this), and render the form from the action state only.

The report names Server Action form state as the likely cause when React's server HTML marks the form as having action state. Without that marker, check whether a browser-only value changed the default.

Why Server Action form state hydration breaks

useActionState can handle a form submitted before JavaScript loads. The server processes the submission and sends a page with the result. When a permalink is set, the browser may navigate there for that response.

The browser must hydrate from that same result. In a custom setup, the formState passed to hydrateRoot must match the server renderer's value. Otherwise useActionState may start from initialState while the HTML shows the submitted state.

That happens when:

  • a custom server or framework integration renders with form state but does not pass it to hydrateRoot;
  • the permalink page renders a different form component, action or permalink than the page the form was on, so React cannot match the state to the form;
  • the form copies the action result into local state or computes default values from something else, so the server and client disagree even with the right state.

How to fix it

Let the framework pass the form state

Next.js handles this handoff. With a custom server, serialize the form state into the page and pass it to hydrateRoot:

client.tsx
import { hydrateRoot } from "react-dom/client";
 
// The same value the server passed to its renderer as `formState`,
// serialized into the page by your server.
const formState = JSON.parse(
  document.getElementById("form-state")?.textContent ?? "null"
);
 
hydrateRoot(document, <App />, { formState });

Render the form from the action state only

Render the form's values from useActionState. The destination page must render the same form, action, and permalink:

app/newsletter/signup.tsx
"use client";
 
import { useActionState } from "react";
import { subscribe } from "./actions";
 
export function Signup() {
  // Before: const [email] = useState(() => readDraftFromStorage());
  const [state, formAction, pending] = useActionState(
    subscribe,
    { email: "", message: "" },
    "/newsletter"
  );
 
  return (
    <form action={formAction}>
      <input id="newsletter-email" name="email" defaultValue={state.email} />
      <button disabled={pending}>Subscribe</button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

React uses that matching form to connect the submitted state to the page.

Keep default values identical on both sides

HP1012 can also mean a control's defaultValue, defaultChecked, or selected option differs for another reason. Use server-known data for initial form values; read storage or browser preferences after hydration.

Catch it with ESLint

A linter cannot verify the framework's form-state handoff. It can catch some browser-only defaults with no-client-only-initial-state and no-storage-in-initial-render.

Catch it in CI

hydration-proof test compares form values, checked state, and selections. Use interactions to test a submission before hydration.

npx hydration-proof test