In React 19 you can pass a function to a <form> element's action prop instead of writing an onSubmit handler. What does React take over on your behalf when you do that?
answer
- React, not you, calls preventDefault
- the argument is not the event
- it runs inside a transition
- pending becomes readable by descendants
- uncontrolled fields clear on success
basics
~20 sReact calls the function with the form's FormData, prevents the default navigation itself, and runs the call as an Action inside a transition — so React tracks pending state, surfaces errors, and resets an uncontrolled form after it succeeds.
solid answer
~50 sThe `action` prop has always accepted a URL string; React 19 also accepts a function. When it is a function, React intercepts the submit for you: it prevents the browser's default navigation, builds a `FormData` object from the form's named fields, and calls your function with it. Because React runs that call inside a transition, the function may be `async` and React keeps the submission marked pending for its whole duration — which is what makes descendant pending UI (via `useFormStatus` from `react-dom`) and optimistic updates possible without threading props. If the function rejects and you do not handle it, React surfaces the error to the nearest error boundary rather than silently dropping it. After a successful function action React resets an uncontrolled form's fields. The contrast with `onSubmit` is ownership: with `onSubmit` you call `preventDefault()`, read the fields, and hand-roll a loading flag; with `action` React owns all four.
code
jsx · 17 linesfunction Subscribe() {
async function subscribe(formData) {
const email = formData.get('email');
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
}
return (
<form action={subscribe}>
<input type="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
);
}go deeper
Know that a function passed to a form's action prop receives a FormData object, that React blocks the browser's default navigation for you, and that the function may be async.
Explain that React runs the action inside a transition, and that this is what makes pending state readable by descendants and optimistic updates possible — not merely a nicer way to read field values.
Be ready to say what React does not do: no double-submit protection, no validation, no retry, and no use of the return value unless the action is routed through useActionState. Name the uncontrolled-form reset and when it hurts.
Own the convention: whether forms in your codebase go through Actions at all, what shape an action's result takes so error rendering is uniform, and how a shared submit component reads pending state without every form reinventing it.
## Two shapes of the same prop On a DOM `<form>`, `action` is an HTML attribute: a URL the browser posts to, causing a full-page navigation. React 19 keeps that meaning when you pass a string, and adds a second meaning when you pass a function. The function form is what React calls an **Action**. When React sees a function, it attaches its own submit handling to the form. On submit it calls `preventDefault()` for you — so the page does not navigate — collects the form's fields into a `FormData` object, and invokes your function with that object as its only argument. Fields are collected by their `name` attribute, exactly as the browser would for a native submission, which means an input without a `name` contributes nothing. ```jsx function Subscribe() { async function subscribe(formData) { const email = formData.get('email'); await fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); } return ( <form action={subscribe}> <input type="email" name="email" /> <button type="submit">Subscribe</button> </form> ); } ``` ## Why it is more than sugar for onSubmit The part that matters is not the `FormData` convenience — it is that React runs the call inside a **transition**. A transition is React's marker for "an update whose result the user is waiting on, but which must not block the interface". Because the submission is a transition, React knows the moment it starts and the moment it ends, including across `await` points inside an `async` action. That single fact is what the rest of the Actions model is built on: - **Pending state becomes ambient.** Any descendant of the form can ask React whether its parent form is currently submitting, using `useFormStatus` (imported from `react-dom`). A shared `SubmitButton` in a design system can render its own spinner without the form threading an `isSubmitting` prop down to it. - **Optimistic updates have a lifetime.** An optimistic value shown during the Action is automatically discarded when the transition finishes, because React knows when that is. - **Errors have a destination.** If the function rejects and nothing catches it, React surfaces the error to the nearest error boundary instead of leaving an unhandled rejection in the console. With a hand-written `onSubmit` you can reproduce every one of these, but each one is code you own: a `try/finally` so the loading flag is not left stuck `true`, a piece of state and a prop chain for the spinner, and your own decision about what an unhandled rejection means. ## What happens after the action resolves For a function action, React resets an **uncontrolled** form after the action completes successfully — the typed values disappear, which is usually what you want for a "post a comment" box and surprising for a long edit form. Controlled inputs are unaffected, because their values live in your state, not in the DOM node. React 19 exposes `requestFormReset` from `react-dom` when you need to control the reset yourself. Nothing else is reset or retried. React does not disable the submit button, does not deduplicate rapid submissions, and does not decide what the result means. Those remain design decisions in your component. ## The pieces you still own An Action is a plumbing primitive, not a form library. You still decide: - **What the action returns.** A returned value only becomes visible state if you route the action through `useActionState`; a bare `<form action={fn}>` discards the return value. - **How failures are shown.** Rejecting is a blunt instrument (it reaches an error boundary); returning an error result is how inline field messages are normally produced. - **Validation.** React does not validate; native constraint attributes such as `required` and `type="email"` still work and still block submission before your function runs. ## Where candidates trip The two most common mistakes are expecting the submit **event** as the argument (it is `FormData`; there is no `event.preventDefault()` to call, and nothing to read `event.target` from) and assuming the function form only works inside a server framework. It does not: a plain client-side function is a perfectly valid action in any React 19 app. What a server framework adds is the ability for the same form to work before hydration, which is a separate property. Finally, a submit `<button>` may carry its own `formAction` function to override the form's, which is the React 19 way to give one form two verbs — "save draft" and "publish" — without inspecting which button was clicked.
- If the action rejects, does React still reset the form?No. The automatic reset applies after a function action completes successfully, so a rejected submission leaves the typed values in place — which is the behaviour you want, since the user usually has to correct something and resubmit. If you need to control resetting yourself, React 19 exposes `requestFormReset` from `react-dom`.
- What changes if you pass a plain URL string to action instead of a function?You get the ordinary HTML behaviour: React adds no handling at all, the browser performs a real navigation and posts the fields to that URL, and the React app on the page is torn down and reloaded. There is no pending state, no optimistic UI, and no return value — the server response is a new document.
- How would you let one form have two different submit actions?Give each submit button its own `formAction` prop. In React 19 `formAction` on a `<button>` or `<input type="submit">` accepts a function and overrides the form's `action` for that button, so a "Save draft" and a "Publish" button can run different functions over the same fields without inspecting which one was pressed.
saying these in an interview costs you the question
- Says you must call event.preventDefault() inside the action
- Expects the submit event as the action's argument
- Thinks the action must be synchronous
- Claims form actions only work in a server framework
- Believes React resets controlled inputs after submission