Forms and Controlled Inputs
Wiring form inputs to state — the controlled-component pattern React uses for text fields, checkboxes, and selects.
2 min read
By default, an HTML <input> keeps its own internal state in the DOM — you'd normally read its value with document.querySelector. React instead encourages making the input a controlled component: its value comes entirely from React state, and every keystroke updates that state through an event handler.
function NameForm() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}This creates a single source of truth. The input never manages its own value independently — on every keystroke, onChange fires, setName updates state, and React re-renders the input with value set back to that same state. Visually it looks like the user is just typing normally, but the displayed value is actually being driven by React on every render, which is what makes it possible to validate, transform, or reset the input's contents from your own code at any time.
Handling multiple fields with one handler
For a form with several fields, a single handler keyed by the input's name attribute avoids writing a separate handler per field:
function SignupForm() {
const [form, setForm] = useState({ email: "", password: "" });
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<>
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
</>
);
}The { ...prev, [name]: value } spread creates a new object with every existing field intact except the one that changed — necessary because, as covered in the useState lesson, state must be replaced with a new object, never mutated in place.
Checkboxes and selects
Checkboxes read and write checked instead of value:
<input
type="checkbox"
checked={form.subscribed}
onChange={(e) => setForm((prev) => ({ ...prev, subscribed: e.target.checked }))}
/>A <select> works the same as a text input, using value on the <select> element itself rather than on each <option>:
<select value={form.plan} onChange={(e) => setForm((prev) => ({ ...prev, plan: e.target.value }))}>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>Submitting the form
onSubmit on the <form> element, combined with e.preventDefault() to stop the browser's default full-page reload, is the standard pattern:
function handleSubmit(e) {
e.preventDefault();
console.log(form); // send it wherever it needs to go
}
<form onSubmit={handleSubmit}>{/* fields */}<button type="submit">Sign up</button></form>;With user input covered, the next lesson looks at fetching data from a server and keeping loading and error states in sync using useEffect.