# Form Handling

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Forms
Source: https://www.devtutorials.ai/learn/react/forms/form-handling

> Controlled components, text, textarea, select, checkbox, radio and special inputs, handling input, many inputs with one handler, form state, submission and resetting.

## Introduction to Forms

A **form** is a part of a page where the user types or chooses information, and then sends it. Login boxes, search bars, sign-up pages and checkout pages are all forms.

A form is made of **inputs** (boxes, lists, tick boxes) and a **submit button**. In React, you keep what the user types in **state**, so your code always knows the current values.

### Real-World Example

A bookshop checkout page has inputs for name, address and a delivery option. Each input is controlled: `value={form.name}` shows the state, and `onChange` updates it.

When the user clicks **Place order**, `onSubmit` calls `e.preventDefault()` and sends the `form` object to the server. The page does not reload.

In React, the values of a form are kept in state. Each input shows the state, and each change updates the state.

## Controlled Components

A **controlled component** is an input whose value is managed by React state.

React controls what appears in the input by storing the value in state and updating it whenever the user types.

To make an input controlled, you need two props:

- **`value`** — tells the input what value to display. It is usually connected to a state variable.
- **`onChange`** — runs whenever the user changes the input. It updates the state with the new value.

Together, `value` and `onChange` keep the input and state in sync.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-controlled-title" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-controlled-title">A controlled input: the user types, onChange saves the text in state, and value shows the state in the box</title>
  <rect x="20" y="60" width="170" height="50" rx="8" style={{ fill: "var(--panel)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="105" y="90" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontFamily: "var(--sans)" }}>Input box</text>
  <rect x="330" y="60" width="170" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="415" y="85" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 14, fontWeight: 700, fontFamily: "var(--sans)" }}>State</text>
  <text x="415" y="102" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 12, fontFamily: "var(--mono)" }}>name = "Asha"</text>
  <path d="M190 70 Q260 20 330 70" style={{ fill: "none", stroke: "var(--faint)", strokeWidth: 2 }} />
  <path d="M322 64 L332 72 L320 74 Z" style={{ fill: "var(--faint)" }} />
  <text x="260" y="30" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontFamily: "var(--mono)" }}>onChange → setName</text>
  <path d="M330 100 Q260 150 190 100" style={{ fill: "none", stroke: "var(--faint)", strokeWidth: 2 }} />
  <path d="M198 106 L188 98 L200 96 Z" style={{ fill: "var(--faint)" }} />
  <text x="260" y="152" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontFamily: "var(--mono)" }}>value={"{name}"}</text>
  <circle r="6" style={{ fill: "var(--accent)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M190 70 Q260 20 330 70 L330 100 Q260 150 190 100 Z" />
  </circle>
</svg>
<figcaption>Typing goes into state with onChange. The box shows the state with value. Round and round.</figcaption>
</figure>

      <label>
        Your name:{" "}
        <input value={name} onChange={(e) => setName(e.target.value)} />
      </label>
      <p>Hello, {name || "reader"}!</p>
      <p>Your name has {name.length} letters.</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Type in the box. The greeting and the letter count change as you type, because they read the same `name` state.

Look at this line:

```tsx
<input value={name} onChange={(e) => setName(e.target.value)} />
```

- `value={name}` makes the box show the `name` state.
- `onChange` runs on every key press. `e.target.value` is the text in the box now. `setName` saves it.

If you set `value` but forget `onChange`, the box is stuck. You cannot type in it, because nothing updates the state.

## Form Input Elements

Each kind of input works in almost the same way: show the state, and update it on change. Only a few details are different.

### Text Input

A **text input** is a one-line box for short text, such as a name. You saw it above.

```tsx
<input
  type="text"
  value={name}
  onChange={(e) => setName(e.target.value)}
/>
```

### Textarea

A **textarea** is a bigger box for long text, such as a review. In React, you set its text with `value`, the same as a text input.

```tsx
<textarea
  value={review}
  onChange={(e) => setReview(e.target.value)}
  rows={4}
/>
```

### Select Dropdown

A **select** shows a list of choices that opens when you click it. Put `value` and `onChange` on the `<select>`, not on the options.

```tsx
<select
  value={genre}
  onChange={(e) => setGenre(e.target.value)}
>
  <option value="fiction">Fiction</option>
  <option value="biography">Biography</option>
</select>
```

### Checkbox

A **checkbox** is a tick box. It is on or off, so its state is `true` or `false`.

A checkbox is different in two ways: use **`checked`** instead of `value`, and read **`e.target.checked`** instead of `e.target.value`.

```tsx
<input
  type="checkbox"
  checked={wantsGift}
  onChange={(e) => setWantsGift(e.target.checked)}
/>
```

### Radio Button

**Radio buttons** are a group of round buttons. The user can choose only **one** of them.

Give every radio in the group the same `name`. Each one is checked when the state matches its own value.

```tsx
<input
  type="radio"
  name="format"
  value="paper"
  checked={format === "paper"}
  onChange={(e) => setFormat(e.target.value)}
/>
<input
  type="radio"
  name="format"
  value="ebook"
  checked={format === "ebook"}
  onChange={(e) => setFormat(e.target.value)}
/>
```

Quick summary of the five inputs:

<div className="tscroll">

| Input | Shows the state with | Reads the new value from | State type |
|---|---|---|---|
| Text input | `value` | `e.target.value` | text |
| Textarea | `value` | `e.target.value` | text |
| Select | `value` on `<select>` | `e.target.value` | text |
| Checkbox | `checked` | `e.target.checked` | `true` / `false` |
| Radio button | `checked={state === value}` | `e.target.value` | text |

</div>

The lab below has all five kinds of input. Try each one and watch the summary change.

      <label>
        Name
        <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      </label>

      <label>
        Review
        <textarea rows={3} value={review} onChange={(e) => setReview(e.target.value)} />
      </label>

      <label>
        Genre
        <select value={genre} onChange={(e) => setGenre(e.target.value)}>
          <option value="fiction">Fiction</option>
          <option value="biography">Biography</option>
          <option value="self-help">Self-help</option>
        </select>
      </label>

      <label className="inline">
        <input type="checkbox" checked={wantsGift} onChange={(e) => setWantsGift(e.target.checked)} />
        Gift wrap
      </label>

      <div className="inline">
        Format:
        <label>
          <input type="radio" name="format" value="paper" checked={format === "paper"} onChange={(e) => setFormat(e.target.value)} />
          Paper
        </label>
        <label>
          <input type="radio" name="format" value="ebook" checked={format === "ebook"} onChange={(e) => setFormat(e.target.value)} />
          E-book
        </label>
      </div>

      <pre className="summary">{JSON.stringify({ name, review, genre, wantsGift, format }, null, 2)}</pre>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 8px; max-width: 320px; font-size: 14px; }
.form label { display: grid; gap: 2px; }
.form .inline, .form .inline label { display: flex; gap: 6px; align-items: center; }
.summary { background: #f1f5f9; padding: 8px; border-radius: 6px; font-size: 12px; margin: 0; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The grey box at the bottom shows all the state at once. It changes the moment you change any input.

### Special Input Types

The `<input>` tag has many types. Each type gives the user a better box for one kind of data.

<div className="tscroll">

| Type | What the user sees | What you read |
|---|---|---|
| `type="email"` | A box for an email address. Phones show the `@` key. | `e.target.value` (text) |
| `type="password"` | A box that hides the letters with dots | `e.target.value` (text) |
| `type="number"` | A box for numbers, with up and down arrows | `e.target.value` (text, so change it with `Number(...)`) |
| `type="date"` | A box with a calendar to pick a date | `e.target.value` (text like `"2026-11-03"`) |
| `type="file"` | A button to choose a file from the computer | `e.target.files` (the chosen files) |

</div>

      <label>Email <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></label>
      <label>Password <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /></label>
      <label>Copies <input type="number" min={1} max={10} value={copies} onChange={(e) => setCopies(Number(e.target.value))} /></label>
      <label>Delivery date <input type="date" value={date} onChange={(e) => setDate(e.target.value)} /></label>
      <label>
        Book cover
        <input type="file" onChange={(e) => setFileName(e.target.files?.[0]?.name ?? "")} />
      </label>

      <ul>
        <li>Email: {email}</li>
        <li>Password length: {password.length}</li>
        <li>Copies: {copies} (total ₹{copies * 299})</li>
        <li>Date: {date || "not chosen"}</li>
        <li>File: {fileName || "none"}</li>
      </ul>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 8px; max-width: 340px; font-size: 14px; }
.form label { display: grid; gap: 2px; }
.form ul { margin: 0; padding-left: 18px; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Look at the number box:

```tsx
onChange={(e) => setCopies(Number(e.target.value))}
```

A box always gives back **text**, even when `type="number"`. `Number(...)` changes the text `"3"` into the number `3`, so `copies * 299` works.

A file input is the one input you **do not** control with `value`. You only read the chosen file in `onChange`. `e.target.files?.[0]?.name` means: the name of the first chosen file, if there is one.

## Handling User Input

Every input change runs a **handler**: a function that saves the new value. You can write it inline, or as a named function.

```tsx
// Inline: good for one short line
<input value={name} onChange={(e) => setName(e.target.value)} />

// Named function: good when you need more than one line
function handleNameChange(e: ChangeEvent<HTMLInputElement>) {
  setName(e.target.value.toUpperCase()); // save it in capital letters
}

<input value={name} onChange={handleNameChange} />
```

Because you save the value yourself, you can **change it** before you save it. The second example turns every letter into a capital letter as the user types.

## Handling Multiple Inputs

A form with many inputs would need many `useState` lines and many handlers. A shorter way is to keep **all values in one object**, and use **one handler** for every input.

The trick is the `name` prop. Give each input a `name` that matches a key in the object. The handler reads `e.target.name` to know which key to update.

      <input name="fullName" placeholder="Full name" value={order.fullName} onChange={handleChange} />
      <input name="phone" placeholder="Phone" value={order.phone} onChange={handleChange} />
      <input name="city" placeholder="City" value={order.city} onChange={handleChange} />
      <pre className="summary">{JSON.stringify(order, null, 2)}</pre>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 8px; max-width: 300px; }
.summary { background: #f1f5f9; padding: 8px; border-radius: 6px; font-size: 12px; margin: 0; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Look at this line in `handleChange`:

```tsx
setOrder({ ...order, [name]: value });
```

- `...order` copies all the old values.
- `[name]: value` changes only one key. If you type in the phone box, `name` is `"phone"`, so only `phone` changes.

The square brackets mean "use the value of `name` as the key". Without them, the key would be the word `name` itself.

## Form State Management

**Form state management** means planning what state your form needs, and keeping it tidy. A simple plan works for most forms:

- **One object for the values** — all the fields the user fills in.
- **One true/false for sending** — `isSubmitting`, so you can disable the button while it sends.
- **One value for the result** — a success message, or an error message.

```tsx
const [values, setValues] = useState({ name: "", email: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState("");
```

Store only what you need. For example, "is the form empty?" does not need its own state. Work it out from `values` when you need it:

```tsx
const isEmpty = values.name === "" && values.email === "";
```

## Form Submission

When the user presses the submit button, the form fires a **submit** event. You handle it with `onSubmit` on the `<form>`.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 600 190" role="img" aria-labelledby="fig-form-submit" style={{ width: "100%", maxWidth: 620, height: "auto" }}>
  <title id="fig-form-submit">Animation: pressing Submit fires onSubmit; preventDefault stops the reload, the values from state are sent to the server, and the page shows a message</title>
  <defs><marker id="fs" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="10" y="20" width="150" height="120" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="85" y="42" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Newsletter form</text><rect x="22" y="52" width="126" height="24" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="85" y="68" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>name: "Asha"</text><rect x="22" y="82" width="126" height="24" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="85" y="98" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>email: "asha@..."</text><rect x="40" y="112" width="90" height="22" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="85" y="127" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Subscribe</text>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.03;0.07;0.93;1" dur="10s" repeatCount="indefinite" /><rect x="36" y="108" width="98" height="30" rx="8" style={{ fill: "none", stroke: "var(--ngc-orange)", strokeWidth: 2.5 }} /><text x="85" y="160" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>user presses Submit</text></g>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.2;0.24;0.93;1" dur="10s" repeatCount="indefinite" /><line x1="162" y1="80" x2="196" y2="80" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#fs)" /><rect x="200" y="20" width="180" height="48" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="290" y="40" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>1. e.preventDefault()</text><text x="290" y="56" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>no page reload</text><g><rect x="215" y="76" width="150" height="24" rx="6" style={{ fill: "var(--ngc-red-soft)", stroke: "var(--ngc-red)", strokeWidth: 1.5 }} /><text x="290" y="92" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>page reload</text><line x1="220" y1="88" x2="360" y2="88" style={{ stroke: "var(--ngc-red)", strokeWidth: 2 }} /></g></g>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.46;0.93;1" dur="10s" repeatCount="indefinite" /><line x1="382" y1="44" x2="416" y2="44" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#fs)" /><rect x="420" y="20" width="170" height="48" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="505" y="40" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>2. Send the values</text><text x="505" y="56" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>sendToServer(state)</text></g>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.64;0.68;0.93;1" dur="10s" repeatCount="indefinite" /><line x1="505" y1="70" x2="505" y2="106" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#fs)" /><rect x="420" y="110" width="170" height="48" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="505" y="130" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>3. Show the result</text><text x="505" y="146" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>"Thanks, Asha!"</text></g>
</svg>
<figcaption>One press of Submit: stop the reload, use the values in state, then show the user what happened.</figcaption>
</figure>

The steps are always the same:

1. Call `e.preventDefault()`, so the page does not reload.
2. Use the values from state. For example, send them to a server.
3. Show the user what happened.

      <input
        placeholder="Name"
        value={values.name}
        onChange={(e) => setValues({ ...values, name: e.target.value })}
      />
      <input
        type="email"
        placeholder="Email"
        value={values.email}
        onChange={(e) => setValues({ ...values, email: e.target.value })}
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Join newsletter"}
      </button>
      {message && <p className="ok">{message}</p>}
    </form>
  );
}
`,
    "App.css": `.form { display: grid; gap: 8px; max-width: 280px; }
.ok { color: #15803d; margin: 0; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Fill in both boxes and press **Join newsletter**. The button says "Sending..." for one second. Then the thank-you message appears, and the console shows the data that was sent.

Pressing **Enter** inside a box also submits the form. You get this for free when you use `onSubmit` on a `<form>`.

Put `onSubmit` on the `<form>`, not `onClick` on the button. Then both the button and the Enter key submit the form.

## Resetting Forms

To **reset** a form means to clear it, so it is ready to fill in again. In a controlled form, you reset it by setting the state back to the starting values.

Keep the starting values in one constant. Then you can use it both to start and to reset.

      <form className="form" onSubmit={handleSubmit}>
        <input
          placeholder="Book title"
          value={values.title}
          onChange={(e) => setValues({ ...values, title: e.target.value })}
        />
        <input
          placeholder="Author"
          value={values.author}
          onChange={(e) => setValues({ ...values, author: e.target.value })}
        />
        <div>
          <button type="submit">Add book</button>{" "}
          <button type="button" onClick={() => setValues(emptyForm)}>Clear</button>
        </div>
      </form>
      <ul>
        {books.map((book, i) => <li key={i}>{book}</li>)}
      </ul>
    </div>
  );
}
`,
    "App.css": `.form { display: grid; gap: 8px; max-width: 280px; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Add a book. The book goes into the list, and both boxes become empty, because `setValues(emptyForm)` put the state back to the start.

Look at the **Clear** button. It has `type="button"`. A button inside a form submits the form by default. `type="button"` stops that, so Clear only clears.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| `value` without `onChange` | You cannot type in the box | Add `onChange` that updates the state |
| Starting state as `undefined` instead of `""` | Console warning about switching from uncontrolled to controlled | Start text state as `""` |
| Using `value` on a checkbox | The tick does not change | Use `checked` and `e.target.checked` |
| Radio buttons with different `name`s | More than one can be chosen | Give all radios in a group the same `name` |
| Using the number box value as a number | `"2" + 1` gives `"21"` | Change it with `Number(e.target.value)` |
| Forgetting `e.preventDefault()` in `onSubmit` | The page reloads and the form is lost | Call it first in the submit handler |
| A Clear button without `type="button"` | Clicking Clear submits the form | Add `type="button"` |
| Forgetting `[ ]` around `name` in `{ [name]: value }` | A new key called "name" is added | Write `[name]: value` |
| Changing the state object directly: `values.name = "Asha"` | The box does not update | Make a copy: `setValues({ ...values, name: "Asha" })` |

</div>

## Check Yourself

## Exercises

## What to Remember

- A controlled input shows state with `value` and updates it with `onChange`. Checkboxes use `checked`.
- Keep many inputs in one state object, and use one handler with `[e.target.name]`.
- Submit with `onSubmit` and `e.preventDefault()`. Reset by setting the state back to the starting values.

Next: Form Validation
