Lesson 9 of 31
Controlled Forms
Making React state the single source of truth for an input's value, the controlled/uncontrolled distinction, handling many fields at once, and the case uncontrolled is required.
Forms are where "the UI reflects the data" gets tested the most, because the browser itself also wants to own an input's value. React's answer is the controlled input pattern: state is the single source of truth, and the DOM element just reflects it.
The controlled pattern
import { useState } from "react";
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(event) => setName(event.target.value)}
/>
);
}Every keystroke fires onChange, which updates state; the new state causes
a re-render; the input's value prop reflects that new state right back.
It looks circular, but it's the same one-way data flow as everything else in
React — the DOM never holds a value React doesn't know about, which means
you can validate, transform, or react to every keystroke from one place:
state.
function NameField() {
const [name, setName] = useState("");
return (
<>
<input value={name} onChange={(e) => setName(e.target.value.toUpperCase())} />
<p>{name.length}/20 characters</p>
</>
);
}The trap: value without onChange
If you set value but never wire up onChange, the field displays the
state correctly but never updates it — from the user's perspective, it's
frozen, no matter how much they type. React logs a console warning for
exactly this ("provided a value prop without an onChange handler"), because
it's almost always a mistake rather than an intentional read-only field
(for a genuinely read-only field, use the readOnly attribute instead,
which suppresses the warning and documents the intent).
Multiple fields, one handler
Repeating a useState and a handler for every field gets old fast. A common
pattern uses one state object and reads which field changed from the event
itself:
function SignupForm() {
const [form, setForm] = useState({ email: "", password: "" });
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<form>
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
</form>
);
}[name]: value is a computed property name — the same JavaScript feature
from the objects lesson in the JavaScript track — and it's what lets one
function handle any number of named fields without hardcoding which one.
Uncontrolled inputs: the other option
An uncontrolled input just lets the DOM manage its own value; you read
it only when you need it, typically via a ref (covered properly in a later
lesson) or by reading FormData on submit:
function QuickForm() {
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
console.log(data.get("email"));
}
return (
<form onSubmit={handleSubmit}>
<input name="email" defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}defaultValue (instead of value) sets the initial value without taking
over ownership of it afterward — the DOM tracks changes on its own from
there. Uncontrolled inputs are simpler and can be a fine choice for a form
you only read once, on submit, with no live validation or per-keystroke
behavior needed.
The one case that's genuinely uncontrolled by necessity
A file input can't be controlled — browsers deliberately don't allow script
to set value on <input type="file">, since a page silently "choosing" a
file the user never selected would be a serious security problem. File
inputs are always read via a ref, never via value/onChange.
Try it yourself
What to remember
- A controlled input's
valuecomes from state, andonChangewrites user input back into that same state — React state is the single source of truth. valuewithoutonChangeproduces a frozen-looking field and a console warning; usereadOnlyif that's actually intentional.- One state object plus a computed property name (
[event.target.name]) handles many fields with a single handler. - Uncontrolled inputs (
defaultValue, read via ref or FormData) are a valid, simpler choice when you don't need live, per-keystroke behavior. - File inputs are uncontrolled by necessity — browsers don't allow scripts to set their value.
Check yourself
4 questions · pass 3/4 to unlock Lifting State Up
1.What makes an input 'controlled' in React?
2.What happens if you set
valueon an input without anonChangehandler?3.What's a clean way to handle many form fields with a single onChange handler?
4.Which kind of input is essentially forced to stay uncontrolled?
4 left to answer