Controlled Components
A controlled input gets its value from React state; an uncontrolled one keeps its own value in the DOM. Knowing the difference explains most “why won’t my input change?” bugs.
On this page · 5 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Explain controlled vs uncontrolled inputs
- Format or restrict input while typing
- Use defaultValue and refs for uncontrolled inputs
- Build reusable controlled components
Explanation#
Controlled: <input value={text} onChange={e => setText(e.target.value)} />. React state is the single source of truth; the input always shows state.
Because every keystroke goes through your handler, you can transform it (upper-case a PAN number, strip non-digits from a phone) or reject it.
Uncontrolled: <input defaultValue="Asha" /> — the DOM keeps the value; read it on submit (FormData or a ref). Simpler, but you cannot react to every change.
Never switch an input between controlled and uncontrolled: value={undefined} → value="text" triggers a React warning. Initialise state to "" rather than undefined.
Your own components can be controlled too: a QuantityPicker with value and onChange props works like a native input.
Syntax#
<input value={phone} onChange={e => setPhone(e.target.value.replace(/\D/g, ''))} /> // controlled
<input name="city" defaultValue="Pune" /> // uncontrolled
Code example: Formatting inputs and a reusable controlled picker#
import { useState } from 'react';
function QuantityPicker({ value, onChange, min = 1, max = 10 }) {
return (
<span>
<button type="button" onClick={() => onChange(Math.max(min, value - 1))} aria-label="Decrease">−</button>
<output aria-live="polite"> {value} </output>
<button type="button" onClick={() => onChange(Math.min(max, value + 1))} aria-label="Increase">+</button>
</span>
);
}
export default function Checkout() {
const [phone, setPhone] = useState('');
const [pan, setPan] = useState('');
const [qty, setQty] = useState(1);
return (
<form>
<label>
Phone
<input
value={phone}
onChange={e => setPhone(e.target.value.replace(/\D/g, '').slice(0, 10))}
inputMode="numeric"
/>
</label>
<label>
PAN
<input value={pan} onChange={e => setPan(e.target.value.toUpperCase())} maxLength={10} />
</label>
<label>Quantity <QuantityPicker value={qty} onChange={setQty} /></label>
<label>City (uncontrolled) <input name="city" defaultValue="Pune" /></label>
<p>Phone: {phone || '–'} · PAN: {pan || '–'} · Qty: {qty}</p>
</form>
);
}
Typing “98-765 43210 99” into Phone shows only “9876543210” (digits, max 10). Typing “abcde1234f” into PAN shows “ABCDE1234F”. The −/+ buttons change the quantity between 1 and 10. The City field keeps its own value and starts as “Pune”.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Phone is controlled: the handler strips non-digits and limits to 10 characters before storing.
- PAN is upper-cased on every keystroke.
- QuantityPicker is a controlled custom component: value in, onChange out, clamped to min/max.
- City is uncontrolled: defaultValue sets the starting value and the DOM manages it.
Real-world use case#
A bank form formats card numbers in groups of four, upper-cases IFSC codes and blocks letters in amount fields — all with controlled inputs.
Common mistakes#
value={x} with a broken or missing onChange makes the field read-only. Check the handler updates the same state.
useState() starts as undefined; React warns when it becomes a string. Start with useState("").
defaultValue is only read on mount. Changing it later does nothing; use a controlled value or a key to reset.
Best practices#
- Controlled for fields with live formatting or validation
- Uncontrolled + FormData for simple forms
- Initialise text state with ""
- Give custom inputs a value/onChange API
Remember this#
Controlled = value + onChange (state is the truth).
Uncontrolled = defaultValue (the DOM is the truth).
Never start a controlled input with undefined.
Practice exercise#
Your task: Create a controlled amount input that accepts only digits and at most one decimal point, and shows the amount with ₹ below it.
Need a hint?
Show solution
import { useState } from 'react';
export default function AmountInput() {
const [amount, setAmount] = useState('');
return (
<>
<input
value={amount}
onChange={e => { if (/^\d*\.?\d*$/.test(e.target.value)) setAmount(e.target.value); }}
inputMode="decimal"
aria-label="Amount"
/>
<p>₹{amount || '0'}</p>
</>
);
}
Mini quiz#
Which input is uncontrolled?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.