Controlled vs uncontrolled components
The Short Answer
A controlled component has its form value managed by React state — React is the single source of truth. An uncontrolled component lets the DOM manage the value internally — you read it when needed using a ref.
The distinction comes down to: who owns the data? In a controlled component, React owns it. In an uncontrolled component, the browser's DOM owns it.
Controlled Components
In a controlled component, the input's displayed value is always driven by React state. Every keystroke triggers a state update via onChange, and the input re-renders with the new value from state.
The data flow looks like this:
- User types a character in the input
- onChange event fires with the new value
- Your handler calls setState with the new value
- React re-renders the component
- The input displays the new value from state
In the controlled form below, notice how every input has a value prop tied to state and an onChange handler that updates that state on every keystroke. React is always in sync with what the user sees. This gives you the power to disable the submit button when fields are empty, validate in real time, or transform input as it's typed.
function ControlledForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Values are already in state — use them directly
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email} // React controls the value
onChange={(e) => setEmail(e.target.value)} // Every change updates state
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" disabled={!email || !password}>
Sign In
</button>
</form>
);
}
Because React always knows the current value, you can do things like instant validation, conditional button disabling, character counting, or formatting the input as the user types — all in real time.
Uncontrolled Components
In an uncontrolled component, the DOM itself holds the input's current value. You don't track every keystroke in state. Instead, you read the value when you need it (typically on form submission) using a ref.
function UncontrolledForm() {
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Read values from DOM only when needed
const email = emailRef.current?.value;
const password = passwordRef.current?.value;
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
ref={emailRef} // DOM manages the value
defaultValue="" // Optional initial value
/>
<input
type="password"
ref={passwordRef}
/>
<button type="submit">Sign In</button>
</form>
);
}
Notice the key differences: we use defaultValue instead of value, there's no onChange handler updating state on every keystroke, and we read the value from the ref only when we need it.
Important distinction
value makes an input controlled (React owns it). defaultValue makes it uncontrolled (DOM owns it, React just sets the initial value). Using value without onChange creates a read-only input that the user can't type in.
Side-by-Side Comparison
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | The DOM |
| Value access | Always available in state | Read via ref when needed |
| Props used | value + onChange | defaultValue + ref |
| Re-renders | Every keystroke triggers a re-render | No re-renders on input change |
| Instant validation | Easy — validate in onChange handler | Hard — need imperative DOM access |
| Dynamic behavior | Easy (disable button, show count, format) | Difficult |
| Boilerplate | More — need state + handler for each field | Less — just a ref |
When to Use Which
Use controlled when:
- You need instant validation (password strength, email format)
- You want to conditionally disable the submit button
- You need to format input as the user types (phone number, credit card)
- Multiple inputs depend on each other (start date must be before end date)
- You need to programmatically set, reset, or clear values
Use uncontrolled when:
- Simple forms where you only need the value on submit
- File inputs (<input type="file"> is always uncontrolled)
- Integrating with non-React libraries that manage their own DOM
- Performance-critical forms with many fields (avoids re-renders)
The Most Common Mistake
The most common bug is accidentally switching between controlled and uncontrolled mid-render. This happens when state starts as undefined (making the input uncontrolled) and then becomes a string on the first keystroke (making it controlled). React detects this mode switch and throws a warning. The fix is simple — always initialize your state with an empty string so the input is controlled from the very first render.
// ❌ Bad — starts uncontrolled, becomes controlled
function BuggyInput() {
const [value, setValue] = useState<string | undefined>(undefined);
return (
<input
value={value} // undefined → uncontrolled, then string → controlled
onChange={(e) => setValue(e.target.value)}
/>
);
// React warns: "A component is changing an uncontrolled input
// to be controlled"
}
// ✅ Good — always controlled from the start
function CorrectInput() {
const [value, setValue] = useState(''); // Empty string, not undefined
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
Rule of thumb
Initialize string inputs with '' (empty string), not undefined or null. This ensures the input is controlled from the very first render.
Why Interviewers Ask This
This question tests your understanding of React's data flow philosophy and how it applies to forms. Interviewers want to see that you know the trade-offs between both approaches, can choose appropriately for different scenarios, and understand the common pitfall of switching between modes.
Quick Revision Cheat Sheet
Controlled: React state drives the value. Use value + onChange.
Uncontrolled: DOM manages the value. Use defaultValue + ref.
Default choice: Controlled — gives you full power over validation and dynamic behavior
File inputs: Always uncontrolled — you can't set a file input's value programmatically
Don't mix: Never switch between controlled and uncontrolled — initialize state as '', not undefined