Handling Forms via Form ActionsEntry 3 of 10

useActionState: Action Results as State

useActionState(action, initialState, permalink?) wraps an action so that whatever it returns becomes component state. It returns [state, formAction, isPending].

Notes

  • Arguments:
    1. action: your function. React now calls it with two arguments: (prevState, formData).
    2. initialState: the state before the first submission, e.g. { errors: null }.
    3. permalink (optional): a URL used only with Server Components, so the form works before JavaScript loads.
  • Returns:
    1. state: the initial state, then whatever the action last returned.
    2. formAction: the wrapped action. Pass this to <form action> or <button formAction>, not your original function.
    3. isPending: true while the action runs (Entry 5).
  • Return a consistent shape every time (e.g. { errors, enteredValues }); the component reads that shape.
  • Return { errors: null } on success to clear old messages.
  • prevState is the previous return value. Use it for counters, step-by-step forms, or "keep what was there."
  • useActionState replaced useFormState from React 19 canary builds (it was in react-dom; it's now in react).

What / Why / When

What

A hook that connects an action's result to rendering.

Why

No separate useState for errors, data, and loading.

When

Any form action whose result the UI must show: validation errors, success messages, server responses.

Use case

Showing the signup errors from Entry 2.

Code compare

React
import { useActionState } from "react";​type SignupState = {  errors: string[] | null;  enteredValues?: ReturnType<typeof validateSignup>["enteredValues"];};​export default function Signup() {  function signupAction(prevFormState: SignupState, formData: FormData): SignupState {    const { errors, enteredValues } = validateSignup(formData);​    if (errors.length > 0) {      return { errors, enteredValues };    }​    return { errors: null };  }​  const [formState, formAction] = useActionState(signupAction, { errors: null });​  return (    <form action={formAction}>      {/* ...inputs (Entry 4) */}​      {formState.errors && (        <ul className="error">          {formState.errors.map((error) => (            <li key={error}>{error}</li>          ))}        </ul>      )}​      <p className="form-actions">        <button type="reset" className="button button-flat">          Reset        </button>        <button className="button">Sign up</button>      </p>    </form>  );}

The action can be defined outside the component when it doesn't need props or context. That keeps it pure and testable.

Angular has no useActionState. The same result is a few signals updated by the submit method:

Angular
export class SignupComponent {  private signupService = inject(SignupService);​  protected errors = signal<string[] | null>(null);  protected isPending = signal(false);​  // form = new FormGroup({...}) from Entry 2​  signup() {    if (this.form.invalid) {      this.form.markAllAsTouched();      return;    }​    this.isPending.set(true);    this.signupService      .signup(this.form.getRawValue())      .pipe(finalize(() => this.isPending.set(false)))      .subscribe({        next: () => this.errors.set(null),        error: (err: HttpErrorResponse) => this.errors.set(err.error?.errors ?? ["Signup failed."]),      });  }}
Angular
@if (errors(); as errors) {<ul class="error">  @for (error of errors; track error) {  <li>{{ error }}</li>  }</ul>}

Differences and gotchas

  • Your note's screenshot shows const [formState, formAction] = … underlined: unused variables until the JSX uses them, not an error.
  • Passing your original function to <form action> skips the hook entirely: the state never updates. Always pass the returned formAction.
  • If the action throws, React shows the nearest error boundary instead of updating state. Return expected errors (validation, 4xx) as state; let only unexpected failures throw.
  • Multiple submissions are queued: each call waits for the previous one, so prevState is always the latest result.

Quick reference

React 19Angular
useActionState(action, initialState)Signals updated in the submit method
stateerrors(), result() signals
formAction(ngSubmit)="signup()"
isPendingisPending signal + finalize()
(prevState, formData)this.form.getRawValue()