The detail that surprised me most about React 19's form handling is how little code it takes away — a form that posts data, resets itself, and shows a child button's pending state used to mean three separate pieces of local state wired together by hand. Now the form element itself carries that behavior, and a component nested three levels deep can read the pending flag without a single prop passed down to it.
The action prop on <form>
React 19's DOM bindings let <form> accept a function directly on its action attribute, not just a URL string. Pass it a function and React intercepts the submit, calls the function with the form's FormData, and — this is the part that used to require a manual reset() call — resets the form's uncontrolled inputs automatically once the action resolves successfully. If the action throws or the returned state signals failure, the form is left as-is so the user doesn't lose what they typed.
async function addComment(prevState: { error?: string }, formData: FormData) {
const text = String(formData.get('comment') || '').trim();
if (!text) return { error: 'Comment cannot be empty' };
const res = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify({ text }),
});
if (!res.ok) return { error: 'Failed to post comment' };
return {}; // no error → React clears the uncontrolled inputs
}
export function CommentForm() {
const [state, formAction] = useActionState(addComment, {});
return (
);
}
useFormStatus avoids prop drilling
The submit button in that example is a separate component, and it never received a pending prop. That's useFormStatus at work — it reads the status of the nearest parent <form> from context, so any component rendered inside that form can ask "is my form currently submitting?" without the parent threading a prop down to it. This matters most once a form has more than one child that cares about pending state: a submit button, a cancel button, and a "saving…" banner can all read the same status independently.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
It has to be called from a component rendered inside the <form>, not the component that renders the form itself — calling it in the same component as the <form> tag will always report no pending submission.
Progressive enhancement, in the right framework
None of this requires JavaScript to have loaded, provided the framework you're using supports server functions — Next.js's App Router is the mainstream example. A form's action can point at a Server Action, and the browser's native form submission (a real HTTP POST, no JS needed) handles the request if the client bundle hasn't hydrated yet. Once React does take over, the same action runs through the client-side machinery described above, with pending state and automatic reset layered on top. In a plain client-rendered React app with no server-function support, this benefit disappears — the action prop still works, but there's no form submission happening before your JS loads.
Without a framework that wires the form's action to an actual server endpoint, a JS-disabled or slow-to-hydrate client just sees a form that does nothing on submit. The pending-state and auto-reset benefits are real in plain React; the "works before JS loads" benefit specifically depends on server function support.
Wrapping up
Form actions in React 19 solve a narrower problem than Actions in general: they're specifically about the ceremony of taking form data, submitting it, and putting the form back in a usable state afterward. The action prop removes the manual FormData wiring, useFormStatus removes the prop drilling for pending state, and — if your framework supports it — the same form can do useful work before your JS bundle has even arrived.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.