How does useActionState manage async actions and form state
`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.
Signature
const [state, formAction, isPending] = useActionState(action, initialState);actionis an async function(prevState, formData) => nextState.initialStateis the initial value ofstate.formActionis a wrapped dispatcher you pass to<form action>(or call directly).isPendingis true while the action is in flight.
Form example
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
"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:
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:
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.