Back to React
React
medium
mid

How does the useActionState hook manage async actions and form state in React?

`useActionState` (React 19) wraps an async server/client action. It returns `[state, dispatch, isPending]` — `state` is the action's last return value, `dispatch` invokes the action (queues across calls), `isPending` reflects in-flight status. Designed for forms: `<form action={dispatch}>` works with progressive enhancement. Replaces a lot of useState + useTransition boilerplate.

3 min read·~10 min to think through

Signature

tsx
const [state, formAction, isPending] = useActionState(action, initialState);
  • action is an async function (prevState, formData) => nextState.
  • initialState is the initial value of state.
  • formAction is a wrapped dispatcher you pass to <form action> (or call directly).
  • isPending is true while the action is in flight.

Form example

tsx
async function saveProfile(prev, formData) {
  const name = formData.get("name");
  try {
    await api.update({ name });
    return { ok: true, name };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

function Profile() {
  const [state, action, pending] = useActionState(saveProfile, { ok: false });
  return (
    <form action={action}>
      <input name="name" required />
      <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>
      {state.error && <p className="error">{state.error}</p>}
      {state.ok && <p className="success">Saved {state.name}</p>}
    </form>
  );
}

The form submits via action; React passes the FormData; pending flips automatically. Works without JavaScript when paired with a server action (progressive enhancement).

With server actions

tsx
"use server";
export async function saveProfile(prev, formData) { ... }

In Next.js App Router, the server action runs on the server. useActionState on the client wires up the response + pending state.

Why it exists

Before useActionState:

tsx
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

async function onSubmit(e) {
  e.preventDefault();
  setPending(true); setError(null);
  try {
    const res = await api.update(...);
    setData(res);
  } catch (err) { setError(err.message); }
  finally { setPending(false); }
}

useActionState collapses this into one hook plus a single state object that is the action's return.

Pending state vs useTransition

isPending is similar to useTransition's pending — both mark in-flight work. useActionState is purpose-built for forms / actions with a return state; useTransition is general.

Optimistic updates

Pair with useOptimistic:

tsx
const [optimistic, addOptimistic] = useOptimistic(messages, (state, msg) => [...state, msg]);
const [state, send] = useActionState(sendMessage, null);

async function onSubmit(fd) {
  addOptimistic({ id: tempId, body: fd.get("body") });
  send(fd);
}

Renders the new message immediately; reconciles when the action returns.

Caveats

  • React 19+ only.
  • Works best with server actions / server components context.
  • The action's return value becomes the new state — design the return shape thoughtfully.
  • Multiple rapid submits queue (only the latest formData wins on re-render).

Interview framing

"useActionState (React 19) is purpose-built for forms backed by async actions. It returns [state, formAction, isPending]: state is the action's last return, formAction is a dispatcher you bind to <form action>, isPending tracks in-flight. Replaces useState + useTransition + manual try/catch + pending flag boilerplate. Combined with server actions, gives you progressive enhancement — forms work without JS. Pair with useOptimistic for optimistic updates. The mental shift: design the action's return as your state shape, not a bag of useState calls."

Follow-up questions

  • Compare useActionState and useTransition.
  • How does it work with server actions?
  • What's useOptimistic and how do they compose?

Common mistakes

  • Treating state as the FormData — it's the action's RETURN.
  • Not handling error in the action's return.
  • Forgetting progressive enhancement story.

Performance considerations

  • Fewer renders than the useState-based form pattern. Server actions remove the need for client JS for the action itself.

Edge cases

  • Rapid submits — last one wins.
  • Action throwing — depends on transport.
  • Stale state on slow networks.

Real-world examples

  • Next.js 15 form patterns, React 19 docs examples, Conform + zod integration.

Senior engineer discussion

Seniors discuss the return-state design, progressive enhancement, and composition with useOptimistic for snappy UX without sacrificing correctness.

Related questions