useActionState: Form Results and Pending Actions
useActionState: Form Results and Pending Actions
A school enrolment form needs to show whether submission is pending and retain the latest validation result. Managing these related values separately can produce contradictory messages.
React 19’s useActionState keeps the result of an Action and provides a dispatcher and pending flag. An Action is work coordinated through a supported Action context, such as a form action or a Transition. The result function receives the previous result before the submitted payload.
Understand
Read the three values returned by the Hook.
Build
Validate FormData and return a clear result.
Check
Distinguish a local Action from a real server save.
Submission produces a new Action result
Submit
The form supplies FormData to its action.
Process
The result function receives previous state and form data.
Display
React supplies the returned state and pending status.
The Action function has a different argument contract
useActionState returns [state, dispatchAction, isPending]. state begins with the initial value and later reflects the Action’s result. dispatchAction invokes the result-producing operation in an Action context; passing it to a form’s action prop supplies that context. isPending reports pending work for this Hook.
The Action function receives previousState as its first argument and FormData as the next argument in this form example. This differs from a plain form action receiving FormData directly. Read fields by their name, validate them and return a result with a consistent shape. The previous result can support a cumulative workflow, but it need not be used in every form.
Return an enrolment result from a local Action
This complete React 19 client example validates a name and returns a message. Promise.resolve makes the operation asynchronous without a remote server. No enrolment is persisted, and the text says Prepared rather than Saved. Entering Maya and submitting eventually displays Prepared enrolment for Maya.
React · JavaScript with JSX
import { useActionState } from 'react';
async function prepare(previous, formData) {
const name = String(formData.get('student') ?? '').trim();
await Promise.resolve();
return { message: name ? 'Prepared enrolment for ' + name : 'Enter a student name' };
}
export default function App() {
const [result, action, pending] = useActionState(prepare, { message: 'Not submitted' });
return <main><h1>Enrolment Action</h1><form action={action}>
<label>Student name<input name="student" defaultValue="Maya" /></label>
<button disabled={pending}>{pending ? 'Preparing' : 'Prepare enrolment'}</button>
</form><p role="status">{result.message}</p></main>;
}Expected screen and behavior
The form begins with Maya and Not submitted. Prepare enrolment eventually displays Prepared enrolment for Maya; no server save occurs.Follow submission rather than reading the input during render
React · Return an enrolment result from a local Action
import { useActionState } from 'react';
async function prepare(previous, formData) {
const name = String(formData.get('student') ?? '').trim();
await Promise.resolve();
return { message: name ? 'Prepared enrolment for ' + name : 'Enter a student name' };
}
export default function App() {
const [result, action, pending] = useActionState(prepare, { message: 'Not submitted' });
return <main><h1>Enrolment Action</h1><form action={action}>
<label>Student name<input name="student" defaultValue="Maya" /></label>
<button disabled={pending}>{pending ? 'Preparing' : 'Prepare enrolment'}</button>
</form><p role="status">{result.message}</p></main>;
}The initial Hook result is {message: Not submitted}. The input’s name lets FormData identify its submitted value. The action dispatcher invokes prepare with the previous result and that payload. React coordinates the pending flag while this Action is running.
The returned object becomes the next result, and the status paragraph describes it. React resets uncontrolled form fields after a successful form Action; returning a validation message is still a normally completed Action. If retaining draft values on validation failure is required, use a deliberate controlled form or supported framework form strategy rather than assume fields are preserved.
Choose expected validation results and unexpected failures deliberately
Missing input is an expected validation outcome and can be returned as a message. An unexpected thrown failure can reach an Error Boundary; returning a fake success hides the problem. A real save must validate and authorize on the server as well as handle the client’s result.
A manually dispatched async Action needs the supported Action context, for example startTransition(() => dispatchAction(payload)). Do not invoke the dispatcher during render. This form uses action={action}, which supplies the context without adding an onSubmit handler.
Try the school task
React · starting example
import { useActionState } from 'react';
async function prepare(previous, formData) {
const name = String(formData.get('student') ?? '').trim();
await Promise.resolve();
return { message: name ? 'Prepared enrolment for ' + name : 'Enter a student name' };
}
export default function App() {
const [result, action, pending] = useActionState(prepare, { message: 'Not submitted' });
return <main><h1>Enrolment Action</h1><form action={action}>
<label>Student name<input name="student" defaultValue="Maya" /></label>
<button disabled={pending}>{pending ? 'Preparing' : 'Prepare enrolment'}</button>
</form><p role="status">{result.message}</p></main>;
}Require a name of at least two characters and return a reasoned error for A. Keep the operation local and label its successful result as preparation.
Worked answer
The Action reads and trims the name, then checks the documented minimum length. It returns one consistent message object. Try A first and Maya next; both outcomes are visible results rather than exceptions pretending to save data.
React · JavaScript with JSX
import { useActionState } from 'react';
async function prepare(previous, formData) {
const name = String(formData.get('student') ?? '').trim();
await Promise.resolve();
return { message: name.length < 2 ? 'Use at least two characters' : 'Prepared: ' + name };
}
export default function App() {
const [result, action, pending] = useActionState(prepare, { message: 'Ready' });
return <main><h1>Name validation Action</h1><form action={action}>
<label>Student name<input name="student" defaultValue="A" /></label>
<button disabled={pending}>Check name</button></form><p role="status">{result.message}</p></main>;
}Expected screen and behavior
Check name with A displays Use at least two characters. A later Maya submission displays Prepared: Maya.Mistakes and boundaries
This API requires React 19 or later. It is not the same as useReducer: its Action can be asynchronous and perform side effects. A reducer used by useReducer should remain pure.
A pending flag is not a server transaction lock or guarantee of exactly-once submission. Real mutations need server-side consistency and duplicate handling appropriate to the operation.
A client Action is not automatically a Server Function. Framework-managed Server Functions have additional serialization, security and deployment requirements.
Check your reasoning
React · Return an enrolment result from a local Action
import { useActionState } from 'react';
async function prepare(previous, formData) {
const name = String(formData.get('student') ?? '').trim();
await Promise.resolve();
return { message: name ? 'Prepared enrolment for ' + name : 'Enter a student name' };
}
export default function App() {
const [result, action, pending] = useActionState(prepare, { message: 'Not submitted' });
return <main><h1>Enrolment Action</h1><form action={action}>
<label>Student name<input name="student" defaultValue="Maya" /></label>
<button disabled={pending}>{pending ? 'Preparing' : 'Prepare enrolment'}</button>
</form><p role="status">{result.message}</p></main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
Keep Action results and pending behavior explicit, and label local preparation honestly. Compare ordinary submission handling and temporary optimistic feedback.
Read the primary reference: React: useActionState.
Reaching the recap and completing the quizzes are separate milestones.