Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~22 min· topic 4 of 7

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 null after 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.

Signup.jsxwhole filejsx
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.

Signup.jsxwhole filejsx
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.

Signup.jsxwhole filejsx
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");
  }
}
Track sending with one statusdiagram
Rendering diagram…

04Disable while sending

A disabled fieldset disables every field inside it in one go, and the button can't be pressed twice.

Signup.jsxwhole filejsx
<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.

Signup.jsxwhole filejsx
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. 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, and onSubmit still runs. A handler on the button's onClick would have nothing to attach to.

  2. 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 left sending.

  3. 3

    Add a Cancel button

    Add <button type="button" onClick={() => setStatus("idle")}>Cancel</button> to the sending form. Then remove type="button". What does clicking Cancel do now? (It submits the form, because a button in a form defaults to submit.)

Code & diagrams

onSubmit, preventDefault and FormData New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

fromEntries: { name: 'Asha', topics: 'css' }
getAll topics: [ 'react', 'css' ]
Sending: status, disabled button, reset New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

sending { message: 'Great course!' }
button while sending: Sending…
sent, field is now "Great course!"
A first look at React 19 form actions React 19+ New tab

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

Press Run to see the page.

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.

terminal
$ Console
── what you'll see ──
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'reset')

Break #2

Forget preventDefault

Delete e.preventDefault() from a submit handler in a real (non-Hectal) app.

terminal
$ Browser
── what you'll see ──
The page reloads with ?name=Asha&topics=react&topics=css in the address bar. All React state is lost.

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 fires submit. form.submit() skips both and navigates directly, so React's onSubmit never runs.

  • ▸

    new FormData(form, submitter) (the second argument is the button that was clicked, e.nativeEvent.submitter) includes that button's name/value, which is how one form can have "Save draft" and "Publish" buttons.

  • ▸

    In React 19, a <form action={fn}> runs fn inside a transition (an Action), so state updates inside it don't block typing, and useFormStatus in a child button can read pending. Automatic reset applies only to uncontrolled fields; requestFormReset(form) from react-dom resets 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. 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 calls form.requestSubmit()), and onSubmit catches 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 a type is a submit button; buttons that do something else need type="button".

  2. 2

    By default the browser submits a form by loading a new page (sending the fields to the action URL, 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 is e.preventDefault(), which cancels that default behaviour; then you send the data yourself with fetch (Phase 14).

  3. 3

    FormData reads every named field: const data = new FormData(e.currentTarget). e.currentTarget is the element the handler is attached to (the form); e.target is where the event started, which for submit is 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 with data.getAll("topics"), which returns an array.

  4. 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 to Sending…. Use try/catch/finally around 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. 5

    Resetting: for uncontrolled fields call form.reset(), which puts every field back to its defaultValue (not necessarily empty). For controlled fields set the state back to its starting values. Careful with async handlers: after an await, React has finished the event and e.currentTarget is null, so save the form first: const form = e.currentTarget;.

  6. 6

    React 19 form actions (a preview; Phase 19 covers them fully). You can pass a function to the form's action prop: <form action={saveAction}>. React calls it with the FormData when the form is submitted, without a page reload and without preventDefault, and when the function finishes React resets the uncontrolled fields automatically. Hooks such as useActionState and useFormStatus then 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

01

Why should you use onSubmit on the form instead of onClick on the button?

02

What does e.preventDefault() do in a submit handler, and what happens without it?

03

How do you turn a form's fields into a plain object, and what's the catch with checkbox groups?

04

Describe a robust way to handle sending a form to a server, including errors and double clicks.

05

What do React 19 form actions change compared with onSubmit?

Practice

01

Write a login form that logs { user, password } as an object on submit without reloading the page.

02

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 onSubmit yourself 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 onSubmit and preventDefault.

  • I can read a form with FormData, Object.fromEntries and getAll.

  • I can show sending, success and error states and disable the button while sending.

  • I save e.currentTarget before any await.

  • I can reset controlled and uncontrolled forms.

  • I know what a React 19 form action does and where Phase 19 covers it.