Master the difference between controlled and uncontrolled form components — when React owns the value vs when the DOM does, how to avoid the most common switching bug, and why top form libraries make opposite choices.
A controlled component is like a puppet — React holds every string. Every keystroke flows through an onChange handler into React state, and the input always shows exactly what React says it should. An uncontrolled component is like a free actor — it holds its own state inside the DOM, and you only ask what it's doing when you actually need to know, using a ref. The core question is: who is the single source of truth for the input's value — React state (controlled) or the DOM itself (uncontrolled)?
A controlled input has two required pieces: a value prop tied to React state, and an onChange handler that updates that state. Every keystroke the user makes flows through the handler, updates state, triggers a re-render, and the input displays the new state value. The DOM never independently holds the current value — React state is always the source of truth.
function ControlledSearch() {
const [query, setQuery] = useState('');
return (
<input
type="text"
value={query} // React state drives the displayed value
onChange={e => setQuery(e.target.value)} // every keystroke updates state
/>
);
}
Because React state is always current, you can read query at any moment without touching the DOM. This makes instant validation, character counters, conditional UI, and programmatic resets straightforward.
An uncontrolled input manages its own value inside the DOM — React doesn't know the current value unless it explicitly reads it via a ref. You set the starting value with defaultValue (not value), and the DOM takes over from there.
function UncontrolledForm() {
const nameRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
console.log(nameRef.current.value); // read on demand
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={nameRef}
defaultValue="Alice" // sets initial DOM value; DOM controls it after that
/>
<button type="submit">Submit</button>
</form>
);
}
Uncontrolled inputs don't re-render on every keystroke because React is completely unaware of the changes. This makes them significantly faster for large forms with many fields.
React tracks whether an input is controlled (has a value prop) or uncontrolled (no value prop) from the first render. Switching between them — by passing undefined as the value — causes a React warning and unpredictable behaviour:
// ❌ Bug: undefined on first render (uncontrolled), string later (controlled)
<input value={user?.name} />
// ✅ Fix: always provide a string — controlled from the first render onwards
<input value={user?.name ?? ''} />
If value can ever be undefined or null, always provide a fallback empty string. This is the most common controlled-component bug in real codebases.
React normalises <textarea> and <select> to follow the same controlled/uncontrolled API as <input>:
// Controlled textarea
<textarea value={bio} onChange={e => setBio(e.target.value)} />
// Controlled select — value on the <select>, not on <option>
<select value={country} onChange={e => setCountry(e.target.value)}>
<option value="in">India</option>
<option value="us">USA</option>
</select>
File inputs are always uncontrolled — this is a browser security restriction, not a React limitation. The browser will never allow JavaScript to programmatically set a file input's value. Read the selected file through a ref or the event object:
// ❌ React will warn — file inputs cannot have a value prop
<input type="file" value={file} />
// ✅ Always read via ref or event
const fileRef = useRef(null);
function handleUpload() {
const file = fileRef.current.files[0];
upload(file);
}
<input type="file" ref={fileRef} />
Use controlled when you need:
Use uncontrolled when:
React Hook Form is built on uncontrolled inputs — fields register via refs, values are read on submit, and only fields with errors re-render. This makes it extremely fast for large forms. Formik uses controlled inputs — every field value lives in Formik's state, enabling per-field validation and easy cross-field logic at the cost of per-keystroke re-renders.
Many developers think uncontrolled inputs are the inferior, 'wrong' way — they're a deliberate trade-off. React Hook Form, one of the most widely used React libraries, is built entirely on uncontrolled inputs because they avoid per-keystroke re-renders across the entire form.
Many developers use value={someUndefinedValue} thinking this makes the input uncontrolled — React treats an initial value of undefined as 'no value prop' (uncontrolled), but if value later becomes a string the input switches to controlled mid-lifecycle. Always use value={someValue ?? ''} to stay consistently controlled.
Many developers try to set a file input's value prop — file inputs are always uncontrolled by browser security design. The browser prevents scripts from setting file input values so a webpage can't silently pre-select files. Read files via ref or event.target.files, always.
Many developers think defaultValue and value are just different names for the same thing — defaultValue sets the DOM value once on mount and hands control to the DOM (uncontrolled). value sets the DOM value from React state on every render (controlled). Confusing them is why inputs appear to 'forget' what the user typed.
Many developers think controlled inputs always cause performance problems — the performance difference is negligible for forms with fewer than ~20 fields. Only optimise with uncontrolled inputs when you've actually measured a keystroke-render bottleneck, not as a premature optimisation.
Many developers add onChange to an input without a value prop thinking that makes it controlled — an input without a value prop is uncontrolled regardless of how many event handlers it has. The value prop is what makes it controlled, nothing else.
Search-as-you-type: a search bar that filters results on every keystroke needs a controlled input — React reads query from state on every keystroke to call the filter function or debounced API.
Character counter: 'You have X characters remaining' requires a controlled input to know the current length on every keystroke without querying the DOM.
Multi-step form wizard: each step passes values to the next via React state — controlled inputs make this natural since values already live in state and can be freely read, validated, and reset.
React Hook Form at scale: a checkout form with 30+ fields (shipping, billing, card details) uses uncontrolled inputs via register() so zero re-renders occur on keystroke — only the field with a validation error re-renders.
File upload UI: drag-and-drop upload components use an uncontrolled file input (required by the browser) alongside a separate useState for the selected file preview — the input is uncontrolled, the preview state is React-controlled.
Autofill edge cases: browser autofill and password managers set input values by directly writing to the DOM, bypassing React's onChange. Controlled inputs handle this via a synthetic 'input' event that React re-raises; some form libraries add specific autofill workarounds.
What is the difference between a controlled and uncontrolled component?
What is react-hook-form and why is it better than manual controlled forms?
Controlled vs uncontrolled — who owns the value?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.