Form Actions & useActionState
Wire forms to functions and track pending, error, and success state with zero boilerplate
React 19 lets a <form> call a function directly through its action prop, and pairs it with useActionState to manage the result, error, and pending state automatically. The manual useState + isLoading + try/catch dance is largely gone.
Forms That Call Functions
In React 19, <form action={fn}> invokes fn with the form's FormData when submitted. React resets the form and handles the wiring:
function Newsletter() {
async function subscribe(formData: FormData) {
const email = formData.get("email")
await saveEmail(email)
}
return (
<form action={subscribe}>
<input name="email" />
<button type="submit">Subscribe</button>
</form>
)
}
No onSubmit, no event.preventDefault(), no reading refs — the action receives FormData directly.
useActionState: Result + Pending in One Hook
Most forms need to show a result ("Subscribed!") and a loading state. useActionState wraps your action and gives you all three:
const [state, formAction, isPending] = useActionState(actionFn, initialState)
state— whatever your action returns (message, errors, data)formAction— the wrapped action to pass to<form action={...}>isPending—truewhile the action is running
function Newsletter() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = String(formData.get("email"))
if (!email.includes("@")) return { error: "Invalid email" }
await saveEmail(email)
return { success: `Subscribed ${email}` }
},
{}
)
return (
<form action={formAction}>
<input name="email" disabled={isPending} />
<button disabled={isPending}>
{isPending ? "Subscribing…" : "Subscribe"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="ok">{state.success}</p>}
</form>
)
}
The action receives the previous state as its first argument — handy for accumulating or comparing across submissions.
Try It Live
This is the exact pattern running as a real component. Submit an email without an “@” to see the error path, and watch the button track isPending:
In Next.js, the action function can be a Server Action ("use server"). The
same useActionState code then runs the mutation on the server, with
progressive enhancement — the form even works before JavaScript loads.
Bonus: useFormStatus for Nested Buttons
A submit button often lives in a separate component from the form. useFormStatus reads the parent form's pending state without prop drilling:
import { useFormStatus } from "react-dom"
function SubmitButton() {
const { pending } = useFormStatus()
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>
}