Back to React
React
easy
mid

What is the difference between controlled and uncontrolled components in React?

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.

5 min read·~10 min to think through

Two ways to wire form inputs in React.

Controlled.

tsx
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 → onChangesetState → re-render → value prop → DOM. React is the source of truth.

Uncontrolled.

tsx
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.

ScenarioPick
Live validation as user typesControlled
Conditional field revealControlled
Format-on-type (phone, money)Controlled
Disable submit until validControlled
Simple "name, email, message → POST" formUncontrolled
File input (<input type="file">)Always uncontrolled — read-only value
100-field form with every keystroke triggering re-rendersUncontrolled (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.

tsx
// 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) vs defaultValue (uncontrolled, initial).
  • <input type="checkbox"> / type="radio": checked vs defaultChecked.
  • <select>: value on <select> (React adapts it to set selected on the right <option>).
  • <input type="file">: always uncontrolled — no value prop; read files via ref or onChange.

Hybrid pattern: react-hook-form.

tsx
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.

Related questions