ProgramGuru

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.

  1. Understand

    Read the three values returned by the Hook.

  2. Build

    Validate FormData and return a clear result.

  3. Check

    Distinguish a local Action from a real server save.

Submission produces a new Action result

  1. Submit

    The form supplies FormData to its action.

  2. Process

    The result function receives previous state and form data.

  3. Display

    React supplies the returned state and pending status.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

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.

What is the first argument received by prepare when used with useActionState?
Choose one answer. You can try again.

Answer and explanation

The previous Action state.

Correct. The submitted FormData follows that state argument.

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.

    Next lesson: useOptimistic: Temporary Feedback While a Save Is Pending →