A controlled component is one where React state is the single
source of truth for the input's value. The displayed value comes from
state; every change must go through state via an onChange handler.
function ControlledInput() {
const [value, setValue] = useState('')
return (
<input
value={value} // state drives the DOM
onChange={e => setValue(e.target.value)} // DOM change updates state
/>
)
}
React keeps the DOM perfectly in sync with state — you can read, validate, or transform every keystroke before it appears.
Rule of thumb: If you need to validate, transform, or react to every change as the user types, use a controlled component.
An uncontrolled component lets the DOM manage its own state. React
does not drive the input's value — you only read it when you need it
(typically on submit) using a ref.
function UncontrolledForm() {
const inputRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
console.log(inputRef.current.value) // read on demand
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="initial" />
<button type="submit">Submit</button>
</form>
)
}
Because there is no React state involved, uncontrolled inputs are simpler to set up but offer less control over intermediate values.
Rule of thumb: Use uncontrolled components for simple forms where you only care about the final submitted value, not intermediate input.
| Prop | Type | Behaviour |
|---|---|---|
value |
Controlled | React always overrides the DOM value; component is controlled |
defaultValue |
Uncontrolled | Sets the initial value once; DOM manages value after that |
// Controlled — React owns the value at all times
<input value={stateValue} onChange={e => setState(e.target.value)} />
// Uncontrolled — React only sets the initial value
<input defaultValue="hello" ref={inputRef} />
Mixing value without onChange makes the input read-only from the
user's perspective (React prevents edits). React will warn you.
Rule of thumb: Use value when React should own the value;
use defaultValue when the DOM should own it after mount.
Prefer controlled components when you need:
- Instant validation (highlight errors as the user types)
- Conditional UI based on current field value
- Dynamic constraints (disable a button until all fields are valid)
- Programmatic value changes (auto-fill, clear on reset)
function EmailField() {
const [email, setEmail] = useState('')
const isValid = email.includes('@')
return (
<>
<input
value={email}
onChange={e => setEmail(e.target.value)}
className={isValid ? 'valid' : 'invalid'}
/>
<button disabled={!isValid}>Subscribe</button>
</>
)
}
Rule of thumb: Most production forms use controlled inputs because they provide the feedback loop needed for good UX.
Uncontrolled inputs are simpler when:
- You only need the value at submit time (e.g. a basic search box)
- Integrating with non-React libraries that manage their own DOM
- Handling file inputs (
<input type="file">is always uncontrolled because the browser controls the file path for security) - Rapid prototyping where wiring up state would slow you down
// File inputs are always uncontrolled
function FileUpload() {
const fileRef = useRef(null)
function handleUpload() {
const file = fileRef.current.files[0]
upload(file)
}
return (
<>
<input type="file" ref={fileRef} />
<button onClick={handleUpload}>Upload</button>
</>
)
}
Rule of thumb: <input type="file"> is always uncontrolled; for
everything else, default to controlled unless simplicity wins.
React logs:
"You provided a
valueprop to a form field without anonChangehandler. This will render a read-only field."
The input is effectively frozen — the user types but the DOM value is
always reset to value by React on every render.
// ❌ Read-only — React overrides every keystroke
<input value="fixed" />
// ✅ Controlled with handler
<input value={state} onChange={e => setState(e.target.value)} />
// ✅ Or explicitly read-only with readOnly
<input value="fixed" readOnly />
Rule of thumb: If you pass value, you must also pass onChange
(or readOnly if intentionally read-only).
Passing null or undefined makes the input uncontrolled — React
treats it the same as omitting value. If you later switch back to a
real value, React warns that you're changing a component from
uncontrolled to controlled.
// ❌ Starts uncontrolled (value is null initially)
const [text, setText] = useState(null)
<input value={text} onChange={e => setText(e.target.value)} />
// React: "changing an uncontrolled input to be controlled"
// ✅ Always start controlled with an empty string
const [text, setText] = useState('')
<input value={text} onChange={e => setText(e.target.value)} />
Rule of thumb: Initialise controlled state with an empty string
('') rather than null or undefined.
The same value/defaultValue pattern applies to <textarea> and
<select>. React normalises them so they behave consistently.
// Controlled textarea
<textarea value={text} onChange={e => setText(e.target.value)} />
// Controlled select
<select value={selected} onChange={e => setSelected(e.target.value)}>
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
// Uncontrolled select with initial selection
<select defaultValue="b" ref={selectRef}>
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
Rule of thumb: Treat <textarea> and <select> exactly like
<input> — use value for controlled, defaultValue for uncontrolled.
Technically yes — via the native form submit event's
e.target.elements map — but ref is the idiomatic React approach.
// Using ref (recommended)
const inputRef = useRef(null)
<input ref={inputRef} />
// Read: inputRef.current.value
// Using the form submit event (also works, no ref needed)
function handleSubmit(e) {
e.preventDefault()
const value = e.target.elements.username.value
}
<form onSubmit={handleSubmit}>
<input name="username" />
</form>
The name-based approach is fine for very simple forms and mirrors
traditional HTML form handling.
Rule of thumb: Use ref for programmatic access during the
component's lifetime; use e.target.elements for one-shot submit
reading.
Technically yes, but it's an anti-pattern. Mixing paradigms makes the code harder to reason about and breaks the "single source of truth" principle.
// ❌ Confusing mix
function Form() {
const [email, setEmail] = useState('') // controlled
const phoneRef = useRef(null) // uncontrolled
function handleSubmit() {
const data = { email, phone: phoneRef.current.value }
// collecting values from two different sources
}
}
// ✅ Consistent — all controlled or all via refs
If you choose controlled, keep all fields controlled. If you use a library like React Hook Form it manages uncontrolled inputs uniformly.
Rule of thumb: Pick one pattern per form and stick to it.
React Hook Form uses uncontrolled inputs by default (via ref),
subscribing to change events without storing every keystroke in React
state. This results in fewer re-renders — the component only
re-renders when validation state changes or on submit.
import { useForm } from 'react-hook-form'
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm()
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* register wires up ref + onChange internally */}
<input {...register('email', { required: true })} />
{errors.email && <span>Email required</span>}
<button type="submit">Sign up</button>
</form>
)
}
For controlled integration use the Controller component or
useController hook.
Rule of thumb: React Hook Form is the pragmatic choice for complex forms — it gives controlled-like DX with uncontrolled performance.
Call the state setter with the original default values. Because the input values are driven by React state, setting state is all you need.
const INITIAL = { name: '', email: '' }
function ProfileForm() {
const [form, setForm] = useState(INITIAL)
function reset() {
setForm(INITIAL) // inputs snap back to empty
}
return (
<form>
<input value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
<input value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} />
<button type="button" onClick={reset}>Reset</button>
</form>
)
}
For uncontrolled forms you need to call inputRef.current.value = ''
for each field manually, which is why controlled inputs are easier to
reset programmatically.
Rule of thumb: Controlled components make reset trivial — just restore state. Uncontrolled forms require manual DOM manipulation.
Store the list in state as an array. Each field reads from its array slot and writes back using its index or a stable id.
function TagsInput() {
const [tags, setTags] = useState(['react', 'hooks'])
function handleChange(index, value) {
setTags(prev => prev.map((t, i) => i === index ? value : t))
}
function addTag() {
setTags(prev => [...prev, ''])
}
function removeTag(index) {
setTags(prev => prev.filter((_, i) => i !== index))
}
return (
<>
{tags.map((tag, i) => (
<div key={i}>
<input value={tag} onChange={e => handleChange(i, e.target.value)} />
<button onClick={() => removeTag(i)}>✕</button>
</div>
))}
<button onClick={addTag}>+ Add</button>
</>
)
}
Rule of thumb: Use stable ids rather than array indices as key
when items can be reordered to avoid React losing focus state.
Controlled components give React full authority over the input value
at the cost of wiring up onChange state; uncontrolled components
let the DOM own the value and are simpler to set up but harder to
integrate with validation and dynamic UI.
Controlled → React state is truth → more wiring, more power
Uncontrolled → DOM is truth → less wiring, less control
The React docs historically recommended controlled components for most cases; today, form libraries (React Hook Form) offer a third path that is uncontrolled under the hood but feels controlled in the API.
Rule of thumb: Default to controlled unless performance or third-party DOM libraries push you toward uncontrolled.
More State and Data Flow interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.