React.js, part 12: React 19 Actions and useActionState — the form lifecycle, finally built in
Part 12from the React.js series · 20 parts in all
By 2025 the biggest React releases were no longer about new rendering features but about finishing half-built abstractions. React 19 completed the form story: actions became a first-class concept, and the "submit, validate, show errors, track pending" loop collapsed into one hook.
An action is any async function passed to a form
You do not need a server for this — an action can be a client function. React calls it, awaits it, and manages the pending state of the form around it:
async function saveQuote(formData) {
const res = await fetch('/api/quotes', { method: 'POST', body: formData });
if (!res.ok) return { error: 'Could not save the quote.' };
return { savedAt: Date.now() }; // becomes `state` below
}
useActionState: state + action + pending, in one call
'use client';
import { useActionState } from 'react';
export function QuoteForm() {
// state - the last return value of the action (undefined until it runs)
// action - pass to <form action={...}>, React handles submit + pending
// pending - true while the action is in flight
const [state, action, pending] = useActionState(saveQuote, null);
return (
<form action={action}>
<input name="customer" required />
<textarea name="body" required />
<button disabled={pending}>{pending ? 'Saving…' : 'Save quote'}</button>
{state?.error && <p role="alert">{state.error}</p>}
{state?.savedAt && <p>Saved.</p>}
</form>
);
}
Three things disappeared: the useState for the result, the isSubmitting
boolean, and the onSubmit handler with preventDefault. All three
are now React's job.
The rest of the family
useFormStatus(part 13) reads the pending state from a child component — useful when the submit button is nested deep inside the form.useOptimistic(part 11) layers on top for instant feedback.- Ref as a prop and document metadata (parts 14–15) round out the same release.
The mental shift
React now owns the transition from "form interacted with" to "action finished". Your components describe what the UI should look like in the resulting state, exactly as they do for every other piece of React state. That consistency is the real feature — the form is no longer a special case with its own imperative encoding. Next: pending states done properly.