Controlled vs uncontrolled components
The question is really about who owns the input’s value — React state, or the DOM. It sounds like a beginner topic and interviewers keep asking it because the wrong default causes real problems at scale: a fifty-field form where every keystroke re-renders the page, or a component that flips from one mode to the other and throws a warning nobody understands.
The two modes
// Controlled: React state is the source of truth. Every keystroke
// runs through React.
const [name, setName] = useState("");
<input value={name} onChange={(e) => setName(e.target.value)} />
// Uncontrolled: the DOM holds the value. React reads it when it needs to.
const ref = useRef<HTMLInputElement>(null);
<input defaultValue="" ref={ref} />
// later: ref.current!.value| Controlled | Uncontrolled | |
|---|---|---|
| Value lives in | React state | the DOM node |
| Re-renders on | every change | never, until read |
| Instant validation | trivial | possible, awkward |
| Reset / prefill | set state | defaultValue or .value = |
| Bulk forms | costly | cheap |
value makes an input controlled; defaultValue leaves it uncontrolled. That
one prop is the entire switch, and mixing them on one element is the source of
the famous warning.
Choosing
Controlled when the UI reacts to the value as it is typed: live validation, a character counter, a search box that filters a list, an input whose format you enforce (a masked phone number), or anything where two inputs depend on each other.
Uncontrolled when you only need the value at submit time and nothing
watches it meanwhile: most plain forms. It is also the only option for
<input type="file"> — its value is read-only for security, so it cannot be
controlled, and the same is true of anything integrating a non-React widget
that manages its own DOM.
The performance argument is real but easy to overstate. A controlled input re-renders its component on every keystroke; that is fine for ten fields and noticeable for two hundred, or when the component tree above it is heavy. Measure before restructuring.
The mixed approach, and where React 19 pushes it
Uncontrolled inputs plus a form-level read on submit is the shape most large
forms end up in. FormData reads every field at once, so there is no ref
bookkeeping:
function Signup() {
async function submit(formData: FormData) {
const email = formData.get("email") as string;
await api.signup({ email });
}
return (
<form action={submit}>
<input name="email" type="email" required />
<button>Sign up</button>
</form>
);
}React 19’s form Actions make this the default: <form action={fn}> hands the
function FormData, useActionState gives you pending and error state, and
the browser’s own validation attributes (required, type="email",
pattern) do the first pass for free. Detail in React 19 — Actions, useActionState, useOptimistic, use(), ref as Prop.
Controlled inputs still belong in that form for the fields that need to be watched. The mix is normal; what is not normal is defaulting every field to controlled out of habit.
Validation placement
Validation is the usual reason people reach for controlled, and it is worth separating the two questions.
- When to validate — on change, on blur, or on submit. On-change is the noisiest and the only one that requires controlled state. On-blur catches most errors with far less churn.
- Where the rule lives — inline in the component, or in a schema (Zod, Valibot) that the server also uses. A shared schema is the answer that scales, and it is indifferent to controlled vs uncontrolled.
Form libraries exist to make this choice for you: React Hook Form is uncontrolled by default with per-field subscriptions, and it is the reason a 300-field form does not lag. Formik is controlled. Knowing which your library is explains its performance profile.
Where it breaks in production
- Switching modes. An input rendered with
value={undefined}and then a string flips from uncontrolled to controlled, and React warns. It usually means state was initialised asundefinedinstead of"". - Controlled input with no
onChange. It becomes read-only, silently, because React resets the DOM to the state value on every render. defaultValuethat changes. It is read once at mount; updating the prop later does nothing. Remount with akeyto reset an uncontrolled form.- Reading a ref during render.
ref.currentis populated after commit; read it in handlers and effects. - A file input in state. Cannot be done; store the
Filefrom the change event and render the input uncontrolled. - Every field controlled in a large form. The page lags on typing, and the fix is not memoisation, it is ownership.
Related
Interview angle 4
- “Controlled or uncontrolled?” - who owns the value. Controlled when the UI reacts as the user types - live validation, dependent fields, a filter; uncontrolled when you only need it at submit, which is most forms. File inputs cannot be controlled at all.
- “What does the controlled/uncontrolled warning mean?” - an input’s
valuewent fromundefinedto a string, so React saw it change mode. Almost always state initialised asundefinedinstead of an empty string. - “Why is a 200-field controlled form slow?” - every keystroke re-renders
the owning component, and with the state high in the tree, a lot below it.
The fix is ownership, not
useMemo: uncontrolled fields read at submit, or a library like React Hook Form that subscribes per field. - “How do React 19 Actions change this?” -
<form action>receivesFormData, so the uncontrolled-plus-read-on-submit pattern becomes the default, with pending and error state fromuseActionState. Follow-up: does that mean never controlled? No - controlled for the fields you watch; the mix is the normal shape.