Topic 5.4
Submitting Forms
In one line
Handle a form with onSubmit on the <form>, stop the page reload with e.preventDefault(), read the fields with FormData and Object.fromEntries, track a sending status so the button is disabled while you wait, and reset the form when it's done.
Think of it like this
Posting a letter. You fill in the letter, seal the envelope and drop it in the box. While it is travelling you don't post the same letter again, and once it arrives you start with a fresh sheet. Submitting a form is the same: collect everything, send it once, show that it's on its way, then clear the form.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Submit
- Sending a form: triggered by a submit button or by pressing Enter in a field.
- onSubmit
- The form's event for "the user submitted me". Put it on
<form>, not on the button. - preventDefault()
- A method on the event that stops the browser's built-in action, here the page reload.
- e.currentTarget
- The element the handler is attached to. In a form's onSubmit, the form. Becomes
nullafter the handler finishes. - Object.fromEntries
- Builds a plain object from key/value pairs, such as the pairs in a FormData.
- getAll
data.getAll("name")returns every value with that name as an array (for checkbox groups).- form.reset()
- A DOM method that puts every field of a form back to its default value.
- Form action
- In React 19, a function passed to
<form action={fn}>. React calls it with the FormData on submit.
Step by step
01Catch the submit on the form
onSubmit fires for a button click and for Enter. Cancel the page reload first.
function handleSubmit(e) {
e.preventDefault();
// ...read and send the data
}
<form onSubmit={handleSubmit}>
<input name="email" />
<button>Sign up</button> {/* type="submit" by default */}
</form>02Turn the fields into an object
FormData collects the named fields; Object.fromEntries makes a plain object. Use getAll for names that repeat.
const data = new FormData(e.currentTarget);
const values = Object.fromEntries(data); // { email: "a@b.c", topics: "css" }
values.topics = data.getAll("topics"); // [ "react", "css" ]03Track sending with one status
The status decides the button text, whether it is disabled, and which message shows.
const [status, setStatus] = useState("idle"); // idle | sending | sent | error
async function handleSubmit(e) {
e.preventDefault();
const form = e.currentTarget; // save it before any await
setStatus("sending");
try {
await sendToServer(Object.fromEntries(new FormData(form)));
setStatus("sent");
form.reset();
} catch {
setStatus("error");
}
}04Disable while sending
A disabled fieldset disables every field inside it in one go, and the button can't be pressed twice.
<fieldset disabled={status === "sending"}>
<input name="email" />
<button>{status === "sending" ? "Sending…" : "Sign up"}</button>
</fieldset>
{status === "error" && <p role="alert">Could not sign up. Try again.</p>}05A look ahead: the React 19 way
With a form action, React does the preventDefault, passes the FormData and resets the form for you. Phase 19 adds pending state with useActionState and useFormStatus.
async function signupAction(formData) {
await sendToServer({ email: formData.get("email") });
}
<form action={signupAction}>
<input name="email" />
<button>Sign up</button>
</form>Try it yourself
- 1
Submit with Enter
In the first example, click into the Name field and press Enter. The handler runs again. Then delete the
<button>and press Enter again: a form with a single text field still submits on Enter, andonSubmitstill runs. A handler on the button'sonClickwould have nothing to attach to. - 2
Make the server fail
In the sending example, clear the textarea and press Send. The fake server rejects, the status becomes
error, and the alert appears. The button comes back because the status leftsending. - 3
Add a Cancel button
Add
<button type="button" onClick={() => setStatus("idle")}>Cancel</button>to the sending form. Then removetype="button". What does clicking Cancel do now? (It submits the form, because a button in a form defaults to submit.)
Code & diagrams
Both topics are ticked, but the object keeps only the last one (`css`). `getAll` returns both. Press Enter inside the Name field: that submits too.
Preview
Console
Nothing logged yet.Expected console output
fromEntries: { name: 'Asha', topics: 'css' }
getAll topics: [ 'react', 'css' ]The default message is sent; while the fake server works, the fieldset is disabled and the button says Sending…. After success `form.reset()` puts the textarea back to its **default value**, `Great course!`, not to empty. Type your own message and send it to see that. Clear the box and press Send to see the error path.
Preview
Console
Nothing logged yet.Expected console output
sending { message: 'Great course!' }
button while sending: Sending…
sent, field is now "Great course!"No onSubmit and no preventDefault: React calls `subscribe` with the FormData and, when it finishes, resets the uncontrolled field to its default. Phase 19 adds pending state and errors.
Preview
Console
Nothing logged yet.Expected console output
action got: asha@example.com
action finished
after the action the field is ""Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use e.currentTarget after await
In the sending example, delete const form = e.currentTarget; and write e.currentTarget.reset() after the await.
Break #2
Forget preventDefault
Delete e.preventDefault() from a submit handler in a real (non-Hectal) app.
Myth vs fact
Myth
Put the submit logic in the button's onClick.
Fact
Put it in the form's onSubmit. It catches Enter key submission too, and it's where native validation (Topic 5.6) runs first.
Myth
form.reset() empties every field.
Fact
It puts each field back to its default value (defaultValue, defaultChecked). Controlled fields aren't changed by it at all; reset their state instead.
Myth
Disabling the button is enough to stop double submissions.
Fact
It helps on the client, but the server should also cope with duplicates (for example with an idempotency key, see /system-design) because networks retry and users refresh.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
requestSubmit()is the programmatic way to submit that behaves like a click: it runs constraint validation and firessubmit.form.submit()skips both and navigates directly, so React'sonSubmitnever runs. - ▸
new FormData(form, submitter)(the second argument is the button that was clicked,e.nativeEvent.submitter) includes that button'sname/value, which is how one form can have "Save draft" and "Publish" buttons. - ▸
In React 19, a
<form action={fn}>runsfninside a transition (an Action), so state updates inside it don't block typing, anduseFormStatusin a child button can readpending. Automatic reset applies only to uncontrolled fields;requestFormReset(form)fromreact-domresets on demand (Topic 19.3). - ▸
Disabled fields are left out of FormData. If you disable a fieldset before reading FormData, its values are gone; read the data first, then set the status.
Remember this
- 1
Put the handler on the form, not the button:
<form onSubmit={handleSubmit}>. A form is submitted when the user clicks a submit button or presses Enter in a text field (or code callsform.requestSubmit()), andonSubmitcatches every one of them, after the browser's built-in validation has passed. A click handler on the button runs before validation and doesn't run at all when a form without a submit button is sent with Enter. Inside a form, a<button>without atypeis a submit button; buttons that do something else needtype="button". - 2
By default the browser submits a form by loading a new page (sending the fields to the
actionURL, or to the same page). In a single-page React app that would throw away all your state. So the first line of the handler ise.preventDefault(), which cancels that default behaviour; then you send the data yourself withfetch(Phase 14). - 3
FormDatareads every named field:const data = new FormData(e.currentTarget).e.currentTargetis the element the handler is attached to (the form);e.targetis where the event started, which forsubmitis also the form.Object.fromEntries(data)turns it into a plain object,{ name: "Asha", email: "..." }, ready to send as JSON. Several fields with the same name (a checkbox group) keep only the last value in that object, so read them withdata.getAll("topics"), which returns an array. - 4
Track the status of sending in state:
"idle","sending","sent"or"error". While sending, disable the submit button (or a whole<fieldset disabled>) so people can't send twice, and change its text toSending…. Usetry/catch/finallyaround the request so an error shows a message and the button always comes back. One status string is clearer than several booleans that can contradict each other (Topic 6.1). - 5
Resetting: for uncontrolled fields call
form.reset(), which puts every field back to itsdefaultValue(not necessarily empty). For controlled fields set the state back to its starting values. Careful withasynchandlers: after anawait, React has finished the event ande.currentTargetisnull, so save the form first:const form = e.currentTarget;. - 6
React 19 form actions (a preview; Phase 19 covers them fully). You can pass a function to the form's
actionprop:<form action={saveAction}>. React calls it with theFormDatawhen the form is submitted, without a page reload and withoutpreventDefault, and when the function finishes React resets the uncontrolled fields automatically. Hooks such asuseActionStateanduseFormStatusthen give you the pending state. Everything in this topic still matters: actions are built on the same form, FormData and status ideas.
Explain it without notes
Why should you use onSubmit on the form instead of onClick on the button?
What does e.preventDefault() do in a submit handler, and what happens without it?
How do you turn a form's fields into a plain object, and what's the catch with checkbox groups?
Describe a robust way to handle sending a form to a server, including errors and double clicks.
What do React 19 form actions change compared with onSubmit?
Practice
Write a login form that logs { user, password } as an object on submit without reloading the page.
Add a status to a form so the button reads Saving… and is disabled for 500 ms after submit (use a fake setTimeout promise), then shows Saved!.
Trade-offs
- ↔
Handling
onSubmityourself gives full control (custom requests, JSON bodies, optimistic UI) but you write the status handling. React 19 form actions remove some of that code, and work best with uncontrolled fields. - ↔
Resetting the form after success is tidy for "send a message" forms but wrong for "edit settings" forms, where people expect to see what they saved.
Done when you can
I handle submission with
onSubmitandpreventDefault.I can read a form with FormData,
Object.fromEntriesandgetAll.I can show sending, success and error states and disable the button while sending.
I save
e.currentTargetbefore anyawait.I can reset controlled and uncontrolled forms.
I know what a React 19 form action does and where Phase 19 covers it.