Actions & useActionState
The pattern React 19 deletes
Section titled “The pattern React 19 deletes”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.
Actions: async functions passed to a form
Section titled “Actions: async functions passed to a form”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. useActionStatereturns[state, formAction, isPending]—stateis whatever your action last returned,formActionis what you pass to<form action={...}>, andisPendingis 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 whole three-piece dance — loading flag, error slot, try/catch/finally — collapses into one hook that can’t forget to reset the spinner.