Controlled vs Uncontrolled Inputs
React form inputs as source of truth — controlled value/onChange, uncontrolled defaultValue/ref, hybrid pitfalls, and when each wins.
- react
- forms
- controlled
- uncontrolled
- inputs
In the DOM, an <input> owns its string. In React you choose who is the source of truth for that string while the component is mounted:
- Controlled — React state (or props) drives
value; every keystroke updates state; the DOM is a mirror. - Uncontrolled — the DOM keeps the value; you read it on demand via a ref (often on submit).
Neither is universally “more React.” Controlled wins when the UI must react to every change. Uncontrolled wins when you only care at submit time and want less re-render traffic.
Official framing: react.dev — sharing state / forms patterns.
Controlled: state is truth
function SearchBox() {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-label="Search"
/>
);
}
Data flow:
- User types →
onChange setQueryschedules re-render- Input receives the new
value
You can always derive UI from query: clear buttons, character counts, debounced fetch, validation messages.
function PriceField() {
const [raw, setRaw] = useState('');
const invalid = raw !== '' && Number.isNaN(Number(raw));
return (
<>
<input
inputMode="decimal"
value={raw}
onChange={(e) => setRaw(e.target.value)}
aria-invalid={invalid}
/>
{invalid && <p role="alert">Enter a number</p>}
</>
);
}
Controlling other form controls
| Control | Controlled props |
|---|---|
| text/email/search | value + onChange |
| textarea | value + onChange |
| select | value + onChange |
| checkbox | checked + onChange |
| radio group | checked={value === option} + onChange |
function RememberMe() {
const [on, setOn] = useState(false);
return (
<label>
<input
type="checkbox"
checked={on}
onChange={(e) => setOn(e.target.checked)}
/>
Remember me
</label>
);
}
Uncontrolled: DOM is truth
function NameForm({ onSubmit }) {
const inputRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
onSubmit(inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} name="name" defaultValue="" aria-label="Name" />
<button type="submit">Save</button>
</form>
);
}
Use defaultValue / defaultChecked for initial DOM state — not value without onChange.
Uncontrolled fits:
- Simple “submit the form” screens
- File inputs (always uncontrolled in practice)
- Integrating non-React widgets that own the DOM
- Performance-sensitive huge forms where per-keystroke React work is measurable and unnecessary
function AvatarPicker({ onFile }) {
return (
<input
type="file"
accept="image/*"
onChange={(e) => onFile(e.target.files?.[0] ?? null)}
/>
);
}
File inputs cannot be fully controlled with a synthetic string value for security reasons.
The hybrid bug: value without onChange
// Locked input — React sets value, user cannot change it
<input value={name} readOnly /> // intentional lock
// Accidental lock — missing onChange
<input value={name} /> // warning in dev; typing does nothing useful
If you pass value, you must either update it from onChange or mark the field read-only intentionally.
Switching from uncontrolled to controlled mid-life (or the reverse) also warns:
// First render: undefined value → uncontrolled
// Later: value="" → controlled
<input value={maybeUndefined} onChange={…} />
// Prefer value={maybeUndefined ?? ''} for always-controlled
Keep a field in one mode for its mounted lifetime.
When controlled is worth the re-renders
- Instant validation / formatting (phone masks, uppercase codes)
- Disable submit until valid
- Multi-field constraints (end date ≥ start date)
- Syncing URL query params with the field
- Dependent fields (country → state list)
- Debounced server search as you type
function TagInput() {
const [value, setValue] = useState('');
const normalized = value.trim().toLowerCase();
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => setValue(normalized)}
/>
);
}
Lifting state vs local controlled state
Controlled does not mean “everything lives in Redux.” It means the input’s value is driven by React state somewhere:
- Local
useStatein the field component - Parent form state
- Form library store
Lift only as far as siblings need to read the value (Props vs State, Lifting State Up).
Form libraries
Libraries (React Hook Form, Formik, etc.) often use uncontrolled or ref-based registration for performance, then surface controlled-like APIs for specific fields. Knowing the native distinction helps you debug when a library field “won’t type” (double control) or “won’t reset” (ref vs state).
Reset patterns
Controlled:
setName('');
Uncontrolled:
formRef.current.reset();
// or remount:
<Form key={formVersion} />
Remount via key resets uncontrolled fields cleanly when switching records.
Footguns
valuewithoutonChange.- Flipping undefined ↔ string (uncontrolled ↔ controlled warning).
- Controlling with state updated asynchronously only — caret jumps or lag if you fight the input.
- Formatting on every keystroke incorrectly — cursor jumps to end; use careful selection restore or format on blur.
- Using
defaultValueandvaluetogether — pick one mode. - Reading
ref.current.valuein render — refs don’t trigger re-render; UI won’t update.
Interview angle
Prompt: “Controlled vs uncontrolled components?”
Strong answer: “Controlled inputs take value from React state and update through onChange, so React is the source of truth every render. Uncontrolled inputs keep DOM state, initialized with defaultValue, read via refs usually on submit. Controlled is better for dynamic validation and dependent UI; uncontrolled is lighter when you only need values at submit. Don’t mix modes on one field, and always handle ok-style consistency for file inputs as uncontrolled.”
Live task: convert a controlled search box to uncontrolled submit form and name one feature you lose (live filtering).
Related on this site
- Props vs State — where values live
- useRef for Values and DOM — reading uncontrolled fields
- Forms in React — broader form patterns
- Keys in Lists — remount to reset
- useEffect Fundamentals — avoid syncing controlled state through effects by default
- Debounce Implementation — controlled search without spamming the network
Further reading
<input>— react.dev- Sharing State Between Components
- React Hook Form docs — common uncontrolled-leaning approach
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.