useActionState: forms without the boilerplate
Three useStates for data, error and loading? React 19 does it with one hook — and the form works even before hydration.
For years a React form looked the same: useState for values, another for the error, another for isLoading, an onSubmit with preventDefault() and a try/finally. React 19 ships useActionState, which folds that whole pattern into one hook.
How it works
useActionState(action, initialState) returns a tuple:
state— the last value returned by the action (initialStateat first),formAction— a function you pass to<form action={…}>,isPending—truewhile the action is running.
The action receives the previous state and the FormData. Whatever it returns becomes the new state.
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
type State = { ok: boolean; message: string } | null;
export function NewsletterForm() {
const [state, formAction, isPending] = useActionState<State, FormData>(
async (_prev, formData) => {
const email = String(formData.get("email") ?? "");
if (!email.includes("@")) return { ok: false, message: "Invalid email" };
return subscribe(email);
},
null,
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? "Sending…" : "Subscribe"}
</button>
{state && <p role="status">{state.message}</p>}
</form>
);
}Why it's better
- No hand-rolled
isLoading. React owns the pending state, so it never gets stuck ontruebecause you forgot afinally. - Actions are queued. If the user clicks twice, the second call receives the result of the first as
prev. - Progressive enhancement. When the action is a Server Function (
"use server"), the form submits even before JavaScript has loaded.
One last tip
After a successful action the form resets itself (for uncontrolled inputs). If you want to keep the values after an error, return them in the state and use defaultValue={state?.values?.email}.