Controlled vs uncontrolled components
By FrontendPro Editorial Team Updated 9/26/2026
Answer
In a controlled component React state is the source of truth for the value. In an uncontrolled one the DOM keeps the value and React only reads it when it needs to.
// controlled — state drives the input, every keystroke re-renders
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
// uncontrolled — the DOM holds it, you read on submit
const ref = useRef();
<input defaultValue="" ref={ref} />
// ref.current.value
Note value versus defaultValue: passing value without onChange gives you a read-only field and a React warning, because you have declared React the owner and then given it no way to update.
Which to pick
| You need | Use |
|---|---|
| validate or format as the user types | controlled |
| disable submit until the form is valid | controlled |
| one field's value depends on another | controlled |
| set the value from elsewhere in the app | controlled |
| a simple form read once on submit | uncontrolled |
| a file input | uncontrolled — value is read-only by spec |
| integrating a non-React widget | uncontrolled |
| a huge form and typing feels laggy | uncontrolled |
The React docs recommend controlled inputs as the default, because the value being in state is what makes the rest of the UI able to react to it. The exceptions above are real, though, and a file input is not optional — the browser forbids setting its value.
The costs on each side
Controlled means a re-render per keystroke. For one field that is nothing; for a form with fifty fields inside a heavy tree it is visible, which is why libraries like React Hook Form deliberately use uncontrolled inputs with refs and subscribe only where needed.
Uncontrolled means the value is not in React, so nothing else can derive from it, and testing means reaching into the DOM.
Two traps
value={undefined} silently makes the input uncontrolled, and value={null} triggers the warning. A field fed from an API that has not loaded yet should get value={data?.name ?? ''}.
Switching between the two across renders is an error — React cannot change the ownership of an input mid-life. Decide per field, once.
Beyond forms
The same distinction applies to your own components. A <Tabs> that accepts value/onChange is controlled; one that accepts defaultValue and manages its own state is uncontrolled. Good component libraries support both, which is done by treating the prop as controlled when it is not undefined:
const [internal, setInternal] = useState(defaultValue);
const isControlled = value !== undefined;
const current = isControlled ? value : internal;
