Controlled Inputs
A controlled input is a form element whose displayed value is driven entirely by React state rather than by the DOM. You bind the input’s value to a state variable and update that state in an onChange handler, so React becomes the single source of truth for what the user sees. This pattern makes input data trivial to read, validate, transform, and reset—at the cost of a small amount of wiring. It is the foundation that the dedicated Forms section builds on.
How a controlled input works
The contract is two-way but deliberate: the input renders whatever is in state, and every keystroke flows back into state through onChange. React then re-renders with the new value, which the input displays. Without that loop, the field would appear frozen.
import { useState } from "react";
function NameField() {
const [name, setName] = useState("");
return (
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
);
}
Here value={name} ties the displayed text to state, and onChange reads the new text from e.target.value and writes it back. The current value of the field is always available as the name variable—no DOM querying required.
The single source of truth
Because state owns the value, you can derive other UI directly from it. The input never holds data that React doesn’t know about, so live previews, character counts, and validation come for free.
function Bio() {
const [bio, setBio] = useState("");
const remaining = 140 - bio.length;
return (
<div>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
maxLength={140}
/>
<p>{remaining} characters left</p>
</div>
);
}
You can also transform input as it arrives—forcing uppercase, stripping spaces, or filtering to digits—simply by changing the value before calling the setter:
onChange={(e) => setPhone(e.target.value.replace(/\D/g, ""))}
Because the value passes through your handler, you control exactly what lands in state. Returning early or sanitizing in
onChangelets you reject characters the user can never even type.
Handling many fields with one handler
Writing a separate handler per field gets tedious. A common pattern is to store related fields in a single object and use a generic handler keyed by the input’s name attribute.
import { useState } from "react";
function SignupForm() {
const [form, setForm] = useState({ email: "", username: "", password: "" });
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
console.log(form);
}
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email} onChange={handleChange} />
<input name="username" value={form.username} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
<button type="submit">Sign up</button>
</form>
);
}
Output:
// After typing and clicking "Sign up":
{ email: "[email protected]", username: "ada", password: "hunter2" }
The computed key [name]: value matches each input’s name to the matching field, and the updater function prev => ({ ...prev }) copies the existing object so the other fields aren’t lost. Resetting the whole form is then a one-liner: setForm({ email: "", username: "", password: "" }).
Checkboxes, selects, and other input types
Different elements expose their value differently. The table below summarizes what to bind for each.
| Element | Bind to | Read from | Notes |
|---|---|---|---|
<input type="text"> | value | e.target.value | Also numbers, email, password |
<input type="checkbox"> | checked | e.target.checked | Boolean, not a string |
<input type="radio"> | checked | e.target.value | Compare to selected value |
<select> | value | e.target.value | Set on <select>, not <option> |
<textarea> | value | e.target.value | Value is a prop, not children |
function Preferences() {
const [opts, setOpts] = useState({ newsletter: false, plan: "free" });
return (
<form>
<label>
<input
type="checkbox"
checked={opts.newsletter}
onChange={(e) =>
setOpts((p) => ({ ...p, newsletter: e.target.checked }))
}
/>
Subscribe
</label>
<select
value={opts.plan}
onChange={(e) => setOpts((p) => ({ ...p, plan: e.target.value }))}
>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
</form>
);
}
The read-only warning
If you set value but forget onChange, React makes the field read-only and warns you in the console. The input shows the state value but ignores typing, because there is no path for changes to flow back.
Output:
Warning: You provided a `value` prop to a form field without an
`onChange` handler. This will render a read-only field. If the field
should be mutable use `defaultValue`. Otherwise, set either `onChange`
or `readOnly`.
To fix it, add an onChange. If you genuinely want a fixed, non-editable field, add readOnly instead. And if you want the DOM to manage the value with only an initial seed—an uncontrolled input—use defaultValue rather than value.
Best Practices
- Always pair
valuewithonChange; together they form the controlled-component contract. - Use one object plus a
name-keyed handler when a form has several related fields. - Read checkboxes from
e.target.checkedand everything else frome.target.value. - Sanitize or transform input inside
onChangeso invalid data never enters state. - Reset forms by setting state back to the initial object, not by touching the DOM.
- Add
readOnlyfor intentionally fixed fields, anddefaultValuefor uncontrolled ones, to silence the read-only warning correctly.