Skip to content

React Tutorial Forms and Validation

Handling Multiple and Dynamic Inputs in React Forms

Lesson 73 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Real forms have many inputs, and sometimes the number of inputs is not fixed: several email addresses, several phone numbers, a list of ingredients. This lesson handles both.

Many fixed inputs with one handler

You saw this in Controlled Forms. The handler copies the state and replaces the key named by the input:

App.jsx
JSX
function App() {
  const [address, setAddress] = React.useState({ street: "", city: "", postcode: "" });

  function handleChange(e) {
    setAddress((prev) => ({ ...prev, [e.target.name]: e.target.value }));
  }

  return (
    <fieldset>
      <legend>Address</legend>
      <input name="street" aria-label="Street" placeholder="Street" value={address.street} onChange={handleChange} />
      <input name="city" aria-label="City" placeholder="City" value={address.city} onChange={handleChange} />
      <input name="postcode" aria-label="Postcode" placeholder="Postcode" value={address.postcode} onChange={handleChange} />
      <p>{[address.street, address.city, address.postcode].filter(Boolean).join(", ")}</p>
    </fieldset>
  );
}

We used the updater form (prev) => ... so rapid typing always builds on the latest object. The fieldset and legend group related fields for assistive technology.

A dynamic list of inputs

When the user can add and remove rows, keep an array in state, and give each row an id:

App.jsx
JSX
let nextId = 3;

function App() {
  const [emails, setEmails] = React.useState([
    { id: 1, value: "a@example.com" },
    { id: 2, value: "" },
  ]);

  function update(id, value) {
    setEmails(emails.map((e) => (e.id === id ? { ...e, value } : e)));
  }

  function add() {
    setEmails([...emails, { id: nextId++, value: "" }]);
  }

  function remove(id) {
    setEmails(emails.filter((e) => e.id !== id));
  }

  return (
    <fieldset>
      <legend>Email addresses</legend>
      {emails.map((email, index) => (
        <div key={email.id} style={{ marginBottom: 6 }}>
          <input
            aria-label={"Email " + (index + 1)}
            value={email.value}
            onChange={(e) => update(email.id, e.target.value)}
          />{" "}
          <button type="button" aria-label={"Remove email " + (index + 1)} onClick={() => remove(email.id)}>
            Remove email {index + 1}
          </button>
        </div>
      ))}
      <button type="button" onClick={add}>Add email</button>
      <p>{emails.length} addresses</p>
    </fieldset>
  );
}

Important details:

  • key={email.id}, never the index. If you remove the first row with index keys, the typed text sticks to the wrong row (Understanding React Keys shows this bug live).
  • Each row’s onChange captures its own id, so editing a row cannot affect others.
  • type="button" on the Add and Remove buttons stops them from submitting a surrounding form.
  • The ids come from a counter, not the array position. Use crypto.randomUUID() when you need ids that never repeat across sessions.

Nested form data

If your data is nested (user.address.city), either flatten the form state and rebuild the object on submit, or update with a nested copy:

App.jsx
JSX
setForm({ ...form, address: { ...form.address, city: e.target.value } });

Flat state is usually easier to handle. See Updating Objects in State.

Which approach for many fields?

SituationApproach
Fixed fieldsOne state object + one name handler
Rows added and removed by the userArray of objects with ids
Dozens of fields, validation rules and dependent fieldsA form library (React Hook Form, TanStack Form) or useReducer

Key points

  • One handler and name keys handle many fixed inputs.
  • Dynamic lists need an array of objects with stable ids, and key={id}.
  • Use type="button" for non-submit buttons inside forms.