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].
react.devuseActionState
Notes
- Arguments:
action: your function. React now calls it with two arguments:(prevState, formData).initialState: the state before the first submission, e.g.{ errors: null }.permalink(optional): a URL used only with Server Components, so the form works before JavaScript loads.
- Returns:
state: the initial state, then whatever the action last returned.formAction: the wrapped action. Pass this to<form action>or<button formAction>, not your original function.isPending:truewhile 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. prevStateis the previous return value. Use it for counters, step-by-step forms, or "keep what was there."useActionStatereplaceduseFormStatefrom React 19 canary builds (it was inreact-dom; it's now inreact).
What / Why / When
- What
A hook that connects an action's result to rendering.
- Why
No separate
useStatefor 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
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:
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."]), }); }}@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 returnedformAction. - 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
prevStateis always the latest result.
Quick reference
| React 19 | Angular |
|---|---|
useActionState(action, initialState) | Signals updated in the submit method |
state | errors(), result() signals |
formAction | (ngSubmit)="signup()" |
isPending | isPending signal + finalize() |
(prevState, formData) | this.form.getRawValue() |