Controlled vs Uncontrolled Components in React
Advertisement
🧩 Scenario
Architecture Walkthrough
Why Controlled Components Make State the Single Source of Truth
In a controlled component, the React state variable is the authoritative value of the input at all times. The value prop ties the DOM element to state, and onChange fires on every keystroke to keep state in sync with what the user typed. This creates unidirectional data flow: state flows down to the input as value, and user interaction flows up through onChange to update state.
React always knows the current value without reading the DOM. This enables real-time validation, conditional rendering based on input content, formatted inputs that transform what the user types (phone number masks, currency formatting), and the ability to programmatically set or reset the field by updating state. None of these are straightforward with uncontrolled components because the DOM is the source of truth there, not React.
Why Uncontrolled Components Exist and When They Are the Right Choice
Uncontrolled components defer all value management to the DOM and read the value imperatively via ref.current.value when needed. This is not a legacy pattern to be avoided — it is the correct choice in specific scenarios. File inputs are the clearest example: the browser controls the file picker and the resulting FileList, and React cannot set value on a file input for security reasons, making it inherently uncontrolled. Large forms with many fields are another case where uncontrolled inputs can reduce re-render overhead if none of the fields need real-time validation or cross-field dependencies. Libraries like React Hook Form exploit this by defaulting to uncontrolled inputs and only reading values on submit.
The Warning React Gives When You Switch Between Modes
React warns when an input switches from uncontrolled to controlled or vice versa during its lifetime. This happens when value starts as undefined (making React treat the input as uncontrolled) and is later set to a string. The fix is to always initialize state with an empty string: useState('') not useState(undefined). The same problem appears when a controlled input's onChange handler is missing: React sets the value via the value prop, the user types, the DOM updates temporarily, then React re-renders and resets the input to the state value, making the input appear read-only and generating a console warning.
Key Code Explained
import { useState, useRef } from 'react';
// Controlled input: React state is the source of truth
function ControlledInput() {
const [value, setValue] = useState(''); // '' not undefined — prevents warning
return (
<div>
{/* value prop makes this controlled; onChange keeps state in sync */}
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Controlled"
/>
{/* State is immediately available for rendering — no DOM read needed */}
<p>Character count: {value.length}</p>
<button disabled={value.trim().length === 0}>Submit</button>
</div>
);
}
// Uncontrolled input: DOM owns the value, ref reads it on submit
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Read the value from the DOM only when submitting
const value = inputRef.current?.value ?? '';
console.log('Submitted:', value);
};
return (
<form onSubmit={handleSubmit}>
{/* No value prop — DOM owns the value. defaultValue sets initial value only */}
<input ref={inputRef} defaultValue="" placeholder="Uncontrolled" />
<button type="submit">Submit</button>
</form>
);
}
// File input: always uncontrolled — browser controls the file picker
function FileUploader() {
const fileRef = useRef<HTMLInputElement>(null);
const handleUpload = async () => {
const file = fileRef.current?.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
await fetch('/api/upload', { method: 'POST', body: formData });
};
return (
<div>
{/* value is always read-only on file inputs — cannot be controlled */}
<input type="file" ref={fileRef} accept="image/*" />
<button onClick={handleUpload}>Upload</button>
</div>
);
}
// Mixed form: controlled text fields + uncontrolled file input
function ProfileForm() {
const [name, setName] = useState('');
const [bio, setBio] = useState('');
const avatarRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData();
formData.append('name', name); // from controlled state
formData.append('bio', bio);
if (avatarRef.current?.files?.[0]) {
formData.append('avatar', avatarRef.current.files[0]); // from ref
}
await fetch('/api/profile', { method: 'POST', body: formData });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<textarea value={bio} onChange={(e) => setBio(e.target.value)} />
<input type="file" ref={avatarRef} accept="image/*" />
<button type="submit">Save profile</button>
</form>
);
}
The mixed form pattern shows the correct way to handle a form with both controlled text fields and a file input. The text fields are controlled because they may need real-time validation. The file input is uncontrolled because it must be. Both are handled in the same submit handler.
Tradeoffs
| Approach | Re-renders on keystroke | Real-time validation | Programmatic reset | File input support |
|---|---|---|---|---|
| Controlled | Yes (per field) | Yes | Yes (setState) | No (file input is always uncontrolled) |
| Uncontrolled | No | Only on submit | No (DOM owns it) | Yes |
| React Hook Form (uncontrolled by default) | Only on submit (or trigger: onChange) | Configurable | Yes (reset()) | Yes |
What Interviewers Actually Check
- Whether you can explain the unidirectional data flow of a controlled input
- Whether you know
useState(undefined)causes the uncontrolled-to-controlled warning - Whether you know that a controlled input without
onChangeproduces a read-only field, not an error - Whether you know file inputs are always uncontrolled and can explain the browser security reason
- Whether you know React Hook Form uses uncontrolled inputs by default and why it is faster on large forms
Follow-Up Questions
- You have a controlled input for a phone number that should display formatted output
(555) 123-4567as the user types raw digits. How do you implement the formatting without causing the cursor to jump to the end of the input on every keystroke? - A form has a file upload field alongside controlled text inputs. How do you handle submission when one field is necessarily uncontrolled and the others are controlled?
- Your controlled form with 20 fields feels sluggish on low-end devices. Profiling shows 20 components re-rendering on every keystroke. What is the architecture change that fixes this without switching to an external library?
- A teammate argues that using
defaultValueinstead ofvalueon all inputs makes the form simpler. Explain the concrete behavioral difference they will encounter when trying to reset the form programmatically after submission. - React Hook Form defaults to uncontrolled inputs but supports a
mode: 'onChange'validation option. When would you use this, and what does it say about the tradeoff between performance and real-time UX?
Common Candidate Mistakes
- Binding
valueto state without providingonChange— the input looks frozen and React logs a warning - Initializing controlled input state with
nullorundefined— the input starts uncontrolled and React warns when it becomes controlled - Using
useRefto store a form field value that needs to update other UI — ref changes are invisible to React - Thinking file inputs can be controlled by setting a value prop — the browser ignores it for security reasons
- Not knowing React Hook Form defaults to uncontrolled, which is why it outperforms Formik on large forms
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you implement a controlled input with
valueandonChangetied touseState? - Can you implement an uncontrolled input with
useRefand read the value on submit? - Can you explain what happens when
valueis provided withoutonChange? - Can you explain why file inputs must always be uncontrolled?
- Can you explain what causes the "uncontrolled to controlled" warning and how to prevent it?
Summary
Controlled components have their value driven entirely by React state: the value prop reflects the state, and onChange updates the state on every keystroke. This makes state the single source of truth and enables real-time validation, formatted inputs, and conditional UI. Uncontrolled components let the DOM own the value; React reads it imperatively via a ref only when needed (typically on submit). Uncontrolled components cause fewer re-renders but cannot support real-time features. File inputs must always be uncontrolled because the browser controls the file picker and does not allow setting the value. Initialize controlled inputs with an empty string, not undefined, to avoid the "uncontrolled to controlled" React warning. React Hook Form defaults to uncontrolled inputs and reads values on submit, which is why it significantly outperforms Formik on large forms.
When are uncontrolled components the right choice?
File inputs must be uncontrolled because the browser controls the file picker and React cannot set their value. Large forms with no real-time validation are a performance case for uncontrolled inputs — React Hook Form defaults to uncontrolled to avoid per-keystroke re-renders across many fields.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement