ReactMedium
Controlled vs Uncontrolled components in React — with examples
Controlled — React state is the source of truth; the form element reflects that state. Uncontrolled — the DOM holds the value; React reads it on demand via a ref.
Controlled — recommended for most forms
function NameInput() {
const [name, setName] = useState('');
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
Every keystroke updates React state. The DOM input mirrors name.
Benefits:
- Validate on every change
- Conditionally enable/disable submit
- Programmatically reset, prefill, transform input
Uncontrolled — read the DOM directly
function NameInput() {
const inputRef = useRef<HTMLInputElement>(null);
function handleSubmit() {
console.log('value is', inputRef.current?.value);
}
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={handleSubmit}>Submit</button>
</>
);
}
defaultValue instead of value. No onChange. The DOM owns the truth; React reads it when needed.
Benefits:
- Less re-rendering on each keystroke (for very large forms)
- Easier integration with non-React libraries that touch the DOM
When to use which
| Scenario | Pick |
|---|---|
| Need live validation as user types | Controlled |
| Need to format input as user types ("1234" → "1,234") | Controlled |
| Need to disable submit until valid | Controlled |
| Form with 50+ fields and re-render perf is an issue | Uncontrolled |
| Quick-and-dirty integration with vanilla JS lib | Uncontrolled |
Hybrid pattern — modern React
Libraries like react-hook-form lean uncontrolled internally for perf, but expose a controlled-feeling API:
import { useForm } from 'react-hook-form';
function CheckoutForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('name', { required: true })} />
{errors.name && <span>required</span>}
<input type="email" {...register('email')} />
<button>Submit</button>
</form>
);
}
React doesn't re-render on every keystroke — react-hook-form uses refs internally — but you still get validation, dirty state, etc.
Common interview trap
// ❌ value without onChange — input becomes read-only
<input value={name} />
// React warns: "You provided a `value` prop without an `onChange` handler."
// Fix — either make it read-only intentionally
<input value={name} readOnly />
// Or controlled
<input value={name} onChange={(e) => setName(e.target.value)} />
File inputs are always uncontrolled
You can't set the value of <input type="file"> from JavaScript (security). Use refs.
const fileRef = useRef<HTMLInputElement>(null);
function upload() {
const file = fileRef.current?.files?.[0];
if (file) doUpload(file);
}
return <input type="file" ref={fileRef} onChange={upload} />;