Controlled vs uncontrolled components
Controlled = React state owns the value (`value={x}` + `onChange`). Uncontrolled = DOM owns the value, read via `ref.current.value` or on submit. Controlled wins for live validation, conditional logic, complex forms. Uncontrolled is faster and simpler for plain forms that just submit once. Don't mix the two on the same input.
Two ways to wire form inputs in React.
Controlled.
const [email, setEmail] = useState("");
<input value={email} onChange={e => setEmail(e.target.value)} />The input's displayed value comes from React state. Every keystroke flows: keypress → onChange → setState → re-render → value prop → DOM. React is the source of truth.
Uncontrolled.
const ref = useRef<HTMLInputElement>(null);
<input ref={ref} defaultValue="" />
// later: const value = ref.current.value;The DOM owns the value. React reads it when needed (typically on submit). defaultValue sets the initial value; React then leaves the input alone.
When to use which.
| Scenario | Pick |
|---|---|
| Live validation as user types | Controlled |
| Conditional field reveal | Controlled |
| Format-on-type (phone, money) | Controlled |
| Disable submit until valid | Controlled |
| Simple "name, email, message → POST" form | Uncontrolled |
File input (<input type="file">) | Always uncontrolled — read-only value |
| 100-field form with every keystroke triggering re-renders | Uncontrolled (or hybrid via react-hook-form) |
The performance argument.
Controlled inputs re-render on every keystroke. For one input, irrelevant. For a 100-field form, every keystroke re-renders the whole form unless you localize state. That's why react-hook-form and similar libraries are popular — they use uncontrolled inputs under the hood with refs, give you a controlled-feeling API, and avoid the re-render cascade.
Don't mix on the same input.
// Warning: switching from uncontrolled to controlled
<input value={maybeUndef} onChange={...} />Going from value={undefined} to value="foo" flips the input's controlled-ness mid-life and React warns. Either provide a default (value={maybeUndef ?? ""}) or stay uncontrolled.
Defaults to remember.
<input>/<textarea>:value(controlled) vsdefaultValue(uncontrolled, initial).<input type="checkbox">/type="radio":checkedvsdefaultChecked.<select>:valueon<select>(React adapts it to setselectedon the right<option>).<input type="file">: always uncontrolled — novalueprop; readfilesvia ref oronChange.
Hybrid pattern: react-hook-form.
const { register, handleSubmit, formState: { errors } } = useForm();
<input {...register("email", { required: true })} />Inputs are uncontrolled, react-hook-form subscribes to changes via DOM events, and exposes a hook API. No re-renders per keystroke; validation runs without re-rendering the parent. Default choice in 2026 for non-trivial forms.
The senior framing. The interview answer isn't "controlled is better." It's: controlled when you need to react to each keystroke, uncontrolled when you don't, and use a form library for anything serious because both modes have ergonomic and performance limits at scale.
Follow-up questions
- •Why does mixing controlled and uncontrolled on the same input warn?
- •How does react-hook-form combine the strengths of both?
- •Why are file inputs always uncontrolled?
- •When does a controlled input become a performance bottleneck?
Common mistakes
- •Switching `value` from `undefined` to a string mid-lifecycle.
- •Trying to set `value` on `<input type="file">` programmatically.
- •Using controlled for a 50-field form and getting per-keystroke re-renders.
- •Reading `ref.current.value` for a controlled input (the source of truth is state).
Performance considerations
- •Controlled inputs cause one render per keystroke; debounce, lift state down, or use a form library.
- •Uncontrolled forms avoid the re-render entirely until submit.
Edge cases
- •IME composition (Chinese, Japanese) — onChange fires per composition step, not per glyph. Use onCompositionEnd for final value.
- •Autofill — browser may set the value without firing React's synthetic event; controlled inputs may need to re-read.
- •<input type="number"> — empty string vs NaN handling.
Real-world examples
- •react-hook-form, Formik, TanStack Form — all built on this distinction.
- •Search boxes with debounced API calls — controlled value + debounced effect.