In one minute
Forms are mostly a browser feature. When a form is submitted, the browser collects every field with a name into FormData and sends it to the form’s action URL: by default with GET, which puts the fields in the URL. React adds one thing on top: you can pass a function as action. React then builds the FormData, stops the page from navigating, calls your function, and resets the form afterwards.
You’ll be able to: pick the right method and encType, explain what each one changes, and use a React form action instead of onSubmit + preventDefault.
1<form action="/submitted.html">2 <input name="storeName" />3 <input name="password" type="password" />4 <input name="logo" type="file" />5 <button type="submit">Create store</button>6</form>- event
<button type="submit">Create store</button>Clicking atype="submit"button submits its form. <input name="storeName" />The browser collects every field that has aname.<form action="/submitted.html">With the default method (GET), the fields go into the URL. Recorded:?storeName=…&password=hunter2&logo=logo.svg. The password is now in the browser history, and the file is only its name.- The browser leaves the page: a full page load.
1<form action="/submitted.html"2 method="POST" encType="multipart/form-data">3 <input name="storeName" />4 <input name="password" type="password" />5 <input name="logo" type="file" />6 <button type="submit">Create store</button>7</form>- event
<button type="submit">Create store</button>The same click. method="POST" encType="multipart/form-data">POSTputs the fields in the request body, andmultipart/form-datasends each field as a part, including the file’s real bytes. Recorded: the server received the SVG.- Still a full page load, unless you stop it with
event.preventDefault().
1function createStore(formData: FormData) {2 log('action received:', Object.fromEntries(formData))3}45<form action={createStore}>6 <input name="storeName" />7 <input name="password" type="password" />8 <input name="logo" type="file" />9 <button type="submit">Create store</button>10</form>- event
<button type="submit">Create store</button>The same click. <form action={createStore}>React builds theFormData, prevents the navigation, and calls your function.log('action received:', Object.fromEntries(formData))Recorded:action received: { storeName: "Mugs & More", … }. No request was sent by the browser.- commit phaseAfterwards React resets the uncontrolled fields. Recorded: the store-name input was empty after submitting, and the URL didn’t change.
The example: “Become a seller”
The product store lets people open their own store. All four lessons share the same fields:
<label htmlFor="store">Store name</label>
<input id="store" name="storeName" type="text" />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" />
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
<label htmlFor="logo">Logo</label>
<input id="logo" name="logo" type="file" accept="image/*" />
<button type="submit">Create store</button>
The recorder typed “Mugs & More”, sam@example.com, hunter2 and attached logo.svg, then submitted each version.
1. The default: GET
r.render(
<form action="/submitted.html">
<Fields />
</form>,
)
The address the browser navigated to:
/submitted.html?storeName=Mugs+%26+More&email=sam%40example.com&password=hunter2&logo=logo.svg
Every field is in the URL, including the password: it ends up in browser history, server logs and analytics. And the file is only its name.
2. POST, but without an encoding
method="POST" moves the fields into the request body. What the server received:
{
"contentType": "application/x-www-form-urlencoded",
"body": "storeName=Mugs+%26+More&email=sam%40example.com&password=hunter2&logo=logo.svg",
"containsFileBytes": false
}
The password left the URL, but the default encoding (application/x-www-form-urlencoded) can only send text, so the file is still just logo.svg.
3. POST + multipart/form-data
r.render(
<form action="/submitted.html" method="POST" encType="multipart/form-data">
<Fields />
</form>,
)
------boundary
Content-Disposition: form-data; name="storeName"
Mugs & More
------boundary
Content-Disposition: form-data; name="email"
sam@example.com
------boundary
Content-Disposition: form-data; name="password"
hunter2
------boundary
Content-Disposition: form-data; name="logo"; filename="logo.svg"
Content-Type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#cba6f7"/></svg>
------boundary
Each field is a separate part, and the logo’s actual bytes (the SVG) are in the body. All three versions still reload the page; with plain onSubmit you’d call event.preventDefault() and build new FormData(event.currentTarget) yourself.
4. A React action
function createStore(formData: FormData) {
const { password, logo, ...rest } = Object.fromEntries(formData)
log('action received:', { ...rest, password: password ? '(hidden)' : '', logo: (logo as File).name })
}
r.render(
<form action={createStore}>
<Fields />
</form>,
)
action received: {"storeName":"Mugs & More","email":"sam@example.com","password":"(hidden)","logo":"logo.svg"}
The URL didn’t change (sameUrl: true) and the store-name field was empty afterwards: React reset the form. No preventDefault, no new FormData. A function action can also be async, and React 19’s useActionState and useFormStatus build on it to show pending states and results (covered in the Suspense module).
How it works
- Only named fields are sent. No
name, no entry inFormData. method:GET(default) → fields in the URL;POST→ fields in the body.encType:application/x-www-form-urlencoded(default, text only) ormultipart/form-data(needed for files).- A function
actionis React-only. HTML attributes are strings; React intercepts the submit when you pass a function. - Labels matter.
htmlFor/id(or wrapping the input in the<label>) gives the input an accessible name and a bigger click target. - Give every button a
type. A<button>inside a form defaults totype="submit".
Common mistakes
- Leaving login or signup forms on GET.
- Uploading files without
encType="multipart/form-data"and receiving only file names. onSubmitwithoutevent.preventDefault(): the page reloads and your state is lost.- Untyped buttons (
<button>Show password</button>) submitting the form.
Interview Q&A
Why not leave a form on the default GET method?
GET serializes every field into the URL. The recording navigated to …&password=hunter2&logo=logo.svg: the password is now in the history and in server logs. POST puts fields in the request body.
Why do file uploads need encType="multipart/form-data"?
The default encoding can only carry text, so a file input is sent as its file name (logo=logo.svg in the recorded POST body). Multipart sends each field as its own part, including the file’s bytes; the server received the SVG’s contents.
What does passing a function to action do?
React calls it with the form’s FormData, prevents the browser’s navigation, and resets the form’s uncontrolled fields when the action finishes. It replaces the onSubmit + preventDefault + new FormData(event.currentTarget) boilerplate. HTML can’t do this: attributes are strings.
How do useActionState and useFormStatus relate to form actions?
Both build on function actions (React 19). useActionState wraps an action and tracks its result and pending state across submissions; useFormStatus lets a component inside the form, such as the submit button, read whether the form is submitting, without passing props.
Related
- Inputs: what each kind of input puts in
FormData. - Error Boundaries: reporting errors thrown in an action.
Sources
- react.dev:
<form>,useActionState,useFormStatus - MDN: Sending form data,
FormData - Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react-dom/components/form