Skip to content

Actions & useActionState

Before Actions, submitting a form meant hand-wiring three pieces of state and remembering to update all of them:

// The old way — lots of manual bookkeeping.
function ChangeName() {
const [name, setName] = useState('');
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await updateName(name);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false); // must not forget this
}
}
// ...form wired to handleSubmit
}

Every async form in the app repeats this: a loading flag, an error slot, a try/catch/finally. It’s boilerplate, and forgetting the finally leaves a spinner stuck forever.

An Action is just an async function you hand to a <form> via the action prop. React calls it on submit, passes it the FormData, and manages the pending state and form reset for you:

<form action={async (formData) => {
await updateName(formData.get('name'));
}}>
<input name="name" />
<button type="submit">Update</button>
</form>

No onSubmit, no preventDefault, no manual FormData assembly. But you still want pending and error state to show the user what’s happening — that’s what useActionState adds.

useActionState: pending, error, and result in one call

Section titled “useActionState: pending, error, and result in one call”

useActionState(action, initialState) wraps your action and returns a three-element tuple: the current state, a wrapped action to pass to the form, and a pending boolean.

import { useActionState } from 'react';
function ChangeName() {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get('name'));
if (error) {
return error; // returned value becomes the new state
}
redirect('/profile');
return null; // success: clear the error
},
null, // initial state
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}

Read the shape carefully — it’s the part people get wrong:

  • The action receives (previousState, formData) and returns the next state.
  • useActionState returns [state, formAction, isPending]state is whatever your action last returned, formAction is what you pass to <form action={...}>, and isPending is managed for you.
sequenceDiagram
  participant U as User
  participant F as form action
  participant A as your async action
  U->>F: submit
  F->>F: isPending = true
  F->>A: action(prevState, formData)
  A-->>F: returns next state
  F->>F: isPending = false, state updated
  F->>U: re-render with new state
The Action lifecycle

The whole three-piece dance — loading flag, error slot, try/catch/finally — collapses into one hook that can’t forget to reset the spinner.

What is a React 19 "Action"?
What does `useActionState(action, initialState)` return?
What arguments does the action passed to useActionState receive?
What boilerplate does useActionState replace?