- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Controlled Inputs
Forms and Inputs
Controlled Inputs
A controlled input has no memory of its own. React state is the value, and typing is just a request to change that state.
The loop
Two props make an input controlled: value reads from state, and onChange writes back to it. Remove either and the input misbehaves in a specific, recognisable way.
Value in, change out
jsx
function NameField() {
const [name, setName] = React.useState("")
return (
<div>
<input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Type your name"
/>
<p>State holds: "{name}"</p>
<button onClick={() => setName("")}>Clear</button>{" "}
<button onClick={() => setName("Ada")}>Set to Ada</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<NameField />)Because state is the source of truth, the buttons can change the input just as easily as typing does. That is the payoff - the value is data you own, not something locked inside the DOM.
The read-only mistake
Pass value without onChange and the input freezes: every keystroke is discarded because state never changes and React re-renders it back to the old value. React warns about this in the console.
Three inputs, one works
jsx
function Inputs() {
const [text, setText] = React.useState("start")
return (
<div style={{ display: "grid", gap: 8, maxWidth: 320 }}>
{/* Frozen: value with no way to change it */}
<label>
Frozen: <input value={text} readOnly />
</label>
{/* Uncontrolled: React does not track it at all */}
<label>
Uncontrolled: <input defaultValue={text} />
</label>
{/* Controlled: value and onChange */}
<label>
Controlled: <input value={text} onChange={(e) => setText(e.target.value)} />
</label>
<p>State: "{text}"</p>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Inputs />)Type in each. The first cannot change. The second changes but React never learns about it. Only the third keeps the state and the screen in agreement.
Each input type
- text, email, password, textarea -
valueandevent.target.value. - checkbox -
checkedandevent.target.checked. - radio -
checked={value === "x"}on each option. - select -
valueon theselectitself, not on an option. - number - still gives you a string; convert deliberately.
Every type in one form
jsx
function AllTypes() {
const [form, setForm] = React.useState({
name: "", agreed: false, plan: "free", size: "m",
})
const update = (field, value) => setForm((f) => ({ ...f, [field]: value }))
return (
<div style={{ display: "grid", gap: 8, maxWidth: 340 }}>
<input value={form.name} onChange={(e) => update("name", e.target.value)} placeholder="Name" />
<label>
<input type="checkbox" checked={form.agreed} onChange={(e) => update("agreed", e.target.checked)} />
{" "}Agree to terms
</label>
<div>
{["free", "paid"].map((plan) => (
<label key={plan} style={{ marginRight: 10 }}>
<input
type="radio"
checked={form.plan === plan}
onChange={() => update("plan", plan)}
/>{" "}
{plan}
</label>
))}
</div>
<select value={form.size} onChange={(e) => update("size", e.target.value)}>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
<pre style={{ background: "#f1f5f9", padding: 8 }}>{JSON.stringify(form, null, 2)}</pre>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<AllTypes />)One update helper and a computed key handles every field. That scales far better than a separate useState and handler per input.
useId for label associations
A label needs an htmlFor matching its input's id, and a hard-coded id breaks the moment the component is rendered twice on one page - two elements share an id and the label points at the wrong one.
Unique ids without a counter
jsx
function Field({ label }) {
const id = React.useId()
return (
<p>
<label htmlFor={id}>{label}</label>{" "}
<input id={id} />
</p>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
<div>
<Field label="First name" />
<Field label="Last name" />
<p style={{ color: "#64748b" }}>
Two instances, two different ids — click either label to focus its own input.
</p>
</div>
)useId is for accessibility attributes, not for list keys. It is stable across server and client rendering, which is exactly what a key generated during render is not.
Never start from undefined
Initialising a field with undefined makes the input uncontrolled on its first render, then controlled once a value arrives. React warns loudly about the switch, and the input loses its cursor position at the moment it changes.
Always give a controlled field a real initial value - an empty string for text, false for a checkbox. If the value comes from a server, render an empty field until it loads rather than passing undefined through.
valueplusonChangemakes an input controlled.valuealone freezes it; neither makes it uncontrolled.- Checkboxes use
checked, notvalue. - Number inputs still hand you a string.
- Start from
""orfalse, neverundefined.
