Skip to content

React Tutorial Forms and Validation

Controlled Forms in React: A Complete Example

Lesson 72 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.

You have met controlled inputs and a simple form. Now let us build a realistic controlled form with several fields and see the pattern that scales: one state object, one change handler, and everything else derived.

The pattern

  1. Keep all field values in one state object.
  2. Update it with one handleChange that uses each input’s name.
  3. Derive things such as “is the form valid” instead of storing them.
  4. Handle onSubmit once, with preventDefault.
App.jsx
JSX
function App() {
  const [form, setForm] = React.useState({
    username: "",
    email: "",
    accepted: false,
  });
  const [result, setResult] = React.useState("");

  function handleChange(event) {
    const { name, value, type, checked } = event.target;
    setForm({
      ...form,
      [name]: type === "checkbox" ? checked : value,
    });
  }

  // Derived, not stored in state
  const isValid = form.username.trim() !== "" && form.email.includes("@") && form.accepted;

  function handleSubmit(event) {
    event.preventDefault();
    if (!isValid) return;
    setResult("Account created for " + form.username);
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: "grid", gap: 10, maxWidth: 280 }}>
      <label>
        Username
        <input name="username" aria-label="Username" value={form.username} onChange={handleChange} />
      </label>
      <label>
        Email
        <input name="email" type="email" aria-label="Email" value={form.email} onChange={handleChange} />
      </label>
      <label>
        <input name="accepted" type="checkbox" checked={form.accepted} onChange={handleChange} />
        I accept the terms
      </label>
      <button type="submit" disabled={!isValid}>Create account</button>
      <p role="status">{result}</p>
    </form>
  );
}

What makes this scale

  • Adding a field means adding one key to the initial state and one input with a name. The handler does not change.
  • Checkboxes need checked, not value; the handler picks the right one with type.
  • isValid is recalculated from state on every render, so the button is disabled or enabled correctly with no extra bookkeeping.

Live preview of the form’s data

Because the data is in state, you can show it anywhere. This is handy while building and debugging:

App.jsx
JSX
function App() {
  const [form, setForm] = React.useState({ name: "", role: "student" });
  const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });

  return (
    <div>
      <input name="name" aria-label="Name" value={form.name} onChange={handleChange} />
      <select name="role" aria-label="Role" value={form.role} onChange={handleChange}>
        <option value="student">Student</option>
        <option value="teacher">Teacher</option>
      </select>
      <h4>Live data</h4>
      <pre>{JSON.stringify(form, null, 2)}</pre>
    </div>
  );
}

Controlled vs uncontrolled for forms

If you do not need live feedback, an uncontrolled form read with FormData on submit is shorter (Form Handling). Choose controlled when you want to validate while typing, format, preview, or enable and disable buttons from the data.

Common mistakes

  • Separate handler per field when a single name-based handler works.
  • value={undefined} at the start: use empty strings to keep inputs controlled.
  • Forgetting name on an input, so the handler writes to the key undefined.
  • Storing isValid or errors in state when they can be calculated; see Derived State.

Key points

  • One state object plus one name-based handler scales to many fields.
  • Derive validity, error text and button state from the form data.
  • Checkboxes use checked; everything else uses value.