A controlled input is a form element whose value is driven by React state — the displayed value is always exactly what is in state, and user input updates state via an onChange handler. This gives React complete control over form data, making it easy to validate on every keystroke, disable the submit button when invalid, and reset the form by resetting state. The alternative, uncontrolled inputs, let the DOM own the value (accessed via refs), which is simpler for trivial forms but harder to control and validate.
Controlled Input Basics
import { useState } from "react";
function SearchBar({ onSearch }) {
const [query, setQuery] = useState("");
return (
<input
type="text"
value={query} // React controls the displayed value
onChange={(e) => setQuery(e.target.value)} // state updated on every keystroke
placeholder="Search posts..."
className="border rounded px-3 py-2 w-full"
/>
);
}
// ── Controlled checkbox ───────────────────────────────────────────────────────
function ToggleSwitch({ label }) {
const [isOn, setIsOn] = useState(false);
return (
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isOn} // use 'checked', not 'value'
onChange={(e) => setIsOn(e.target.checked)}
/>
{label}
</label>
);
}
// ── Controlled select ─────────────────────────────────────────────────────────
function StatusFilter({ onFilter }) {
const [status, setStatus] = useState("all");
return (
<select
value={status}
onChange={(e) => {
setStatus(e.target.value);
onFilter(e.target.value);
}}
>
<option value="all">All Posts</option>
<option value="published">Published</option>
<option value="draft">Draft</option>
</select>
);
}
Note: The
onChange event in React fires on every character change — it is equivalent to the DOM’s input event, not the DOM’s change event (which only fires on blur for text inputs). This means React’s onChange for text inputs provides real-time updates while the user is typing, enabling live search, character count displays, and per-keystroke validation.Tip: For forms with many fields, use a single state object and a generic handler that updates the right field by name:
function handleChange(e) { setForm(prev => ({ ...prev, [e.target.name]: e.target.value })); }. Each input needs a name attribute matching the state key. This avoids creating a separate change handler for each field. The [e.target.name] computed property key uses the input’s name as the object key to update.Warning: A controlled input with
value={someValue} but no onChange handler creates a read-only input — the user cannot type into it and React logs a warning. This is a common mistake when you forget to add the handler. If you intentionally want a read-only input, use the readOnly attribute explicitly. If you want an input whose initial value you set but then let the DOM manage, use defaultValue (uncontrolled).Multi-Field Form with Single State Object
function PostCreateForm({ onSubmit }) {
const [form, setForm] = useState({
title: "",
body: "",
status: "draft",
});
const [errors, setErrors] = useState({});
// Generic handler — works for all text/select fields
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
// Clear the error for this field when user starts typing
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: null }));
}
function validate() {
const newErrors = {};
if (!form.title.trim()) newErrors.title = "Title is required";
if (form.title.length < 3) newErrors.title = "Title must be at least 3 characters";
if (!form.body.trim()) newErrors.body = "Body is required";
return newErrors;
}
function handleSubmit(e) {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
onSubmit(form);
setForm({ title: "", body: "", status: "draft" }); // reset on success
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title">Title</label>
<input
id="title"
name="title"
value={form.title}
onChange={handleChange}
className={errors.title ? "border-red-500" : ""}
/>
{errors.title && <p className="text-red-500 text-sm">{errors.title}</p>}
</div>
<div>
<label htmlFor="body">Body</label>
<textarea
id="body"
name="body"
value={form.body}
onChange={handleChange}
rows={6}
className={errors.body ? "border-red-500" : ""}
/>
{errors.body && <p className="text-red-500 text-sm">{errors.body}</p>}
</div>
<div>
<label htmlFor="status">Status</label>
<select id="status" name="status" value={form.status} onChange={handleChange}>
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
</div>
<button
type="submit"
disabled={!form.title.trim() || !form.body.trim()}
>
Save Post
</button>
</form>
);
}
Common Mistakes
Mistake 1 — Controlled input without onChange (read-only, React warning)
❌ Wrong — user cannot type, React logs warning:
<input value={title} /> // Warning: You provided a `value` without an `onChange` handler
✅ Correct — always pair value with onChange:
<input value={title} onChange={(e) => setTitle(e.target.value)} /> // ✓
Mistake 2 — Using value for checkbox (should use checked)
❌ Wrong:
<input type="checkbox" value={isChecked} /> // value is for text inputs!
✅ Correct:
<input type="checkbox" checked={isChecked} onChange={(e) => setIsChecked(e.target.checked)} />
Quick Reference
| Input Type | State Prop | onChange Value |
|---|---|---|
| text, email, password, textarea | value={str} |
e.target.value |
| checkbox | checked={bool} |
e.target.checked |
| select | value={str} |
e.target.value |
| number | value={num} |
Number(e.target.value) |
| Multi-field form | One object | [e.target.name]: e.target.value |