Runnable lesson · press Try it on the examples to edit and run them here.
React 19 added a new way to handle forms: you pass a function to the form’s action prop. React calls it with the form data and takes care of the common chores for you: no preventDefault, automatic pending state and automatic reset of uncontrolled fields.
The action prop
function App() {
const [message, setMessage] = React.useState("");
function subscribe(formData) {
const email = formData.get("email");
setMessage("Subscribed: " + email);
}
return (
<form action={subscribe}>
<input name="email" type="email" aria-label="Email" required />
<button type="submit">Subscribe</button>
<p role="status">{message}</p>
</form>
);
}
Compare with a classic handler:
onSubmit | action |
|---|---|
Receives an event; you call preventDefault() | Receives a FormData; React prevents the reload |
You read values from state or FormData | Values come from the form’s named inputs |
| Fields keep their text unless you reset them | Uncontrolled fields reset automatically after the action |
| You track pending state yourself | The action can be async, and React tracks pending state |
Inputs need a name because the values come from FormData.
useActionState: result and pending state
useActionState wraps an action and gives you the latest result, a wrapped action to pass to the form, and an isPending flag:
const [state, formAction, isPending] = useActionState(actionFn, initialState);
actionFn(previousState, formData) returns the next state.
function App() {
const [state, formAction, isPending] = React.useActionState(async (prev, formData) => {
const name = formData.get("name");
await new Promise((r) => setTimeout(r, 700)); // pretend to call a server
if (!name) return { ok: false, message: "Please enter a name." };
return { ok: true, message: "Welcome, " + name + "!" };
}, { ok: null, message: "" });
return (
<form action={formAction}>
<h3>Join the list</h3>
<input name="name" aria-label="Name" />
<button type="submit" disabled={isPending}>{isPending ? "Joining..." : "Join"}</button>
<p role="status" style={{ color: state.ok === false ? "#b71c1c" : "inherit" }}>{state.message}</p>
</form>
);
}
While the async action runs, isPending is true, so you can disable the button. When it finishes, state holds whatever the action returned. In your own project this is where a server call goes.
useFormStatus: pending state in child components
useFormStatus (imported from react-dom) lets a component inside a form read the form’s status without props. It is ideal for a reusable submit button:
import { useFormStatus } from "react-dom";
function SubmitButton({ children }) {
const { pending } = useFormStatus(); // must be rendered INSIDE a <form>
return <button type="submit" disabled={pending}>{pending ? "Saving..." : children}</button>;
}
function App() {
const [saved, setSaved] = React.useState(false);
async function save() {
await new Promise((r) => setTimeout(r, 600));
setSaved(true);
}
return (
<form action={save}>
<p>Save profile</p>
<SubmitButton>Save</SubmitButton>
{saved && <p>Saved</p>}
</form>
);
}
useFormStatus reads the parent form, so calling it in the same component that renders the <form> does not work; it needs a child component, as above.
useOptimistic: show the result first
useOptimistic shows the expected result immediately while the real request is in progress, and replaces it with the real state when done (or rolls back on failure). It is useful for likes, comments and todo lists. See the React documentation for the full API.
Actions are not only for forms
Any function passed to startTransition can be an Action: React tracks its pending state and keeps the UI responsive. Form action props are the most visible use.
When to choose which style
onSubmit+ controlled state: you need live validation, formatting or changing fields from code.action+FormData: a simple submit with fields you only need on submit, especially with servers and frameworks that run actions on the server (such as a React framework with Server Functions).- A form library: many fields with dependent rules.
In a client-only Vite app the action runs in the browser, exactly like the examples here. Frameworks can run the same pattern on the server, which is why the API looks the way it does.
Key points
- Pass a function to
<form action={fn}>; it receivesFormDataand uncontrolled fields reset automatically. useActionStatereturns the result, a form action and a pending flag.useFormStatusreads the pending state from a child inside a form.