Must knowReact Fundamentals · 07

Forms

One seller form submitted three ways: GET leaks the password into the URL, multipart sends the file, a React action does it all for you.

On this page · 7 sections
  1. In one minute
  2. The example: “Become a seller”
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

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.

Default (GET)
SellerForm.tsx
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>
What you see
Create store
Request: –
Page: the form
  1. event<button type="submit">Create store</button>Clicking a type="submit" button submits its form.
  2. <input name="storeName" />The browser collects every field that has a name.
  3. <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.
  4. The browser leaves the page: a full page load.

0 / 0
The same “Become a seller” form submitted three ways (all three recorded).

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

Common mistakes

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.

Sources

Primary source: react.dev/reference/react-dom/components/form