Controlled Forms

One Object of State for Many Inputs

Controlled Forms

For forms with multiple inputs, hold values in a single object keyed by the input's `name`.

4 min read Level 2/5 #react#forms#controlled
What you'll learn
  • Wire many inputs to one piece of state
  • Submit a form and prevent the default
  • Reset and validate at form level

For a single input, one useState is fine. For a form with five fields, you don’t want five useStates. Keep one object keyed by field name.

The Pattern

function SignupForm() {
  const [form, setForm] = useState({ name: "", email: "", age: "" });

  function update(e) {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  }

  function handleSubmit(e) {
    e.preventDefault();
    console.log("submit", form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name"  value={form.name}  onChange={update} />
      <input name="email" value={form.email} onChange={update} type="email" />
      <input name="age"   value={form.age}   onChange={update} type="number" />
      <button>Sign up</button>
    </form>
  );
}

One onChange for every input. The name attribute on each input tells update which field to set.

Why name on the Input?

Two reasons:

  1. The e.target.name lets one handler service every field
  2. If the form ever falls back to a native submit (e.g., JS off), the field names are what the browser sends

Submit Logic

onSubmit on the <form> fires when the user presses Enter inside an input or clicks a <button> (not <button type="button">). e.preventDefault() keeps the browser from navigating, so you can take over.

function handleSubmit(e) {
  e.preventDefault();
  if (!form.name.trim()) {
    setError("Name required");
    return;
  }
  send(form);
}

Reset

Set the state back to the initial object:

const initial = { name: "", email: "", age: "" };
const [form, setForm] = useState(initial);

function reset() {
  setForm(initial);
}

Validating

Validate any time — on submit, on blur, or live as the user types. Derived from state, no extra storage:

const errors = {
  name: form.name.trim() ? "" : "Required",
  email: form.email.includes("@") ? "" : "Invalid email",
};
const isValid = !errors.name && !errors.email;

return (
  <form onSubmit={handleSubmit}>
    {/* ... inputs ... */}
    <button disabled={!isValid}>Sign up</button>
  </form>
);

When To Reach For a Library

For long forms, async validation, or complex schemas, react-hook-form is what most teams use. The vanilla pattern above is plenty for short forms (which most are).

Up Next

A reminder about updating arrays and objects in state — and the mistake that breaks everything.

Immutable Updates →