A controlled input's value is driven entirely by React state — the DOM input never has a value React doesn't already know about:
function NameForm() {
const [name, setName] = useState("");
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
Every keystroke fires onChange, updates state, and React re-renders the input with that state as its value — from the outside, it looks instantaneous. The alternative, an uncontrolled input, lets the DOM manage its own value and you read it out on demand via a ref (inputRef.current.value) — useful for simple forms or integrating with non-React code, but you lose the ability to validate or react to every keystroke.
useState per fieldfunction 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" value={form.password} onChange={handleChange} />
</>
);
}
A single object with one generic handleChange scales better than a separate useState per field once a form has more than two or three inputs — computed key ([name]: value) lets one handler cover every field, matched by each input's name attribute. For anything beyond a simple form (validation, touched/error state, nested fields), a dedicated form library (React Hook Form, Formik) is the practical real-world choice rather than hand-rolling all of this.