useActionState wraps an Action with state management. The result of every submit becomes new state. Pending is tracked automatically. No more setIsLoading dances.
The signature
const [state, formAction, isPending] = useActionState(actionFn, initialState) where actionFn is (prevState, formData) => Promise<newState>. The hook returns:
- state — the latest state (initial on first render, then whatever the last action returned).
- formAction — the wrapped function you bind to
<form action={...}>. - isPending — true while the action is in-flight.
The reducer-shaped action
Your actionFn looks like a useReducer reducer that also accepts FormData. The first argument is the previous state; the second is the FormData submitted. The return value (or resolved promise value) becomes the new state.
Why this shape? Because errors and success use the same return path. You return { error: '...', input: ... } on failure (preserving the user's input for re-display) and { data: ... } on success. The component decides what to render based on what's in state.
The 'previous state has the form values' pattern
When validation fails server-side, you want to re-display the form with the user's submitted values. Return them as part of the error state. The form's uncontrolled inputs will be re-populated by React (it uses the state's input fields as defaultValue values).
useActionState vs useState
useState gives you arbitrary state with arbitrary setters. useActionState gives you state that's tied to a single action's success/failure cycle. They compose: useState for client-only UI state (drafts, toggles), useActionState for submit-driven state.