Forms and controlled inputs

React · Essentials · 12 min

Forms are where most user input enters an app, and the controlled/uncontrolled distinction shapes how you handle it.

There are two ways to wire an input to React.

A controlled input gets its value from state and reports changes through onChange:

const [name, setName] = useState("");
<input value={name} onChange={e => setName(e.target.value)} />

React state is the single source of truth, so you can validate as the user types, format input, disable the submit button, or reset the field by setting state. Checkboxes use checked and e.target.checked; <select> and <textarea> use value just like inputs.

An uncontrolled input keeps its own value in the DOM. You give it an initial defaultValue (or defaultChecked) and read it when you need it, typically on submit with FormData:

function handleSubmit(e) {
  e.preventDefault();
  const data = new FormData(e.currentTarget);
  save(data.get("email"));
}

Uncontrolled forms are less code and work well when you only care about the values at submit time. In React 19 you can also pass a function to the form's action prop; React calls it with the FormData and resets the form afterwards. That's the basis of Actions, covered at the Advanced level.

Gotchas:

  • Setting value without onChange makes a read-only input (React warns).
  • Don't switch an input between controlled and uncontrolled: value={undefined} then value="x" triggers a warning. Initialise state to "", not undefined.
  • Every input should have a name (for FormData) and a <label> for accessibility.

Example

import { useState } from "react";

export default function SignupForm() {
  const [email, setEmail] = useState("");
  const [agree, setAgree] = useState(false);
  const valid = email.includes("@") && agree;

  function handleSubmit(e) {
    e.preventDefault();                          // no page reload
    const data = new FormData(e.currentTarget);
    console.log(Object.fromEntries(data));       // { email, plan, agree }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input name="email" value={email} onChange={e => setEmail(e.target.value)} />
      </label>
      <label>
        Plan
        <select name="plan" defaultValue="free">   {/* uncontrolled */}
          <option value="free">Free</option>
          <option value="pro">Pro</option>
        </select>
      </label>
      <label>
        <input type="checkbox" name="agree" checked={agree}
               onChange={e => setAgree(e.target.checked)} />
        I agree
      </label>
      <button disabled={!valid}>Sign up</button>
    </form>
  );
}

Remember

  • Controlled: value + onChange bound to state; React state is the source of truth
  • Uncontrolled: defaultValue and read values on submit with new FormData(form)
  • Call e.preventDefault() in onSubmit (or use the React 19 action prop)

Watch out for

  • Initialising controlled-input state to undefined, which flips the input from uncontrolled to controlled.

Check yourself

  1. An <input value={text} /> has no onChange handler. What happens when the user types?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/reference/react-dom/components/input