Who Owns the State?
Controlled vs Uncontrolled
A controlled component takes its value from the parent. An uncontrolled one stores its own. Sometimes both are the right call.
What you'll learn
- Build a controlled component
- Build an uncontrolled one with `defaultValue`
- Recognize "dual-mode" components
A controlled component takes its value as a prop. The parent
owns it. An uncontrolled component manages its own state and
exposes a defaultValue for the initial value.
You’ll see this distinction everywhere — <input> is the canonical
example.
Controlled
function NameField({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
// usage
const [name, setName] = useState("");
<NameField value={name} onChange={setName} /> - Parent owns the value
- Parent can validate, transform, reset
- Every keystroke triggers a re-render in the parent
Uncontrolled
function NameField({ defaultValue, onCommit }) {
return (
<input
defaultValue={defaultValue}
onBlur={e => onCommit(e.target.value)}
/>
);
}
// usage
<NameField defaultValue="" onCommit={save} /> - The input owns the value internally
- The parent only sees the committed result (on blur, on submit, etc.)
- Fewer re-renders in the parent
Trade-offs
| Concern | Controlled | Uncontrolled |
|---|---|---|
| Live validation / formatting | Easy | Hard |
| External “reset to value X” | Easy | Hard |
| Re-render cost | Higher | Lower |
| Code at the call site | More | Less |
Dual Mode
A common library pattern: support both. If value is provided,
treat as controlled; otherwise, manage internally.
function Toggle({ value, defaultValue = false, onChange }) {
const [internal, setInternal] = useState(defaultValue);
const isControlled = value !== undefined;
const current = isControlled ? value : internal;
function update(next) {
if (!isControlled) setInternal(next);
onChange?.(next);
}
return (
<button onClick={() => update(!current)}>
{current ? "On" : "Off"}
</button>
);
} Caller can do either:
<Toggle defaultValue={true} onChange={save} /> // uncontrolled
<Toggle value={open} onChange={setOpen} /> // controlled Up Next
A pre-hooks pattern still worth knowing — render props.
Render Props →