Controlled vs Uncontrolled Components: The Decision You Keep Making Wrong

Ask ten React developers how they handle form inputs and nine of them will say: useState for the value, onChange to update it, pass value and onChange as props. Controlled components, every time, without thinking about it.
That pattern works. It's the one all the tutorials use, it's what React developers learn first, and for many forms it's exactly right.
But it's not always right. And defaulting to it without understanding the tradeoff means you're carrying complexity you don't always need — and sometimes creating bugs you don't need to create.
What "controlled" and "uncontrolled" actually mean
The question at the center of this distinction is: who owns the value?
In a controlled component, React owns the value. The input's current value is stored in React state. Every keystroke updates the state. The rendered value is always whatever state says it is.
In an uncontrolled component, the DOM owns the value. The input manages its own state internally the way any HTML input does. React can read the value when it needs to, but it doesn't track every change.
Notice defaultValue instead of value. That's the signal: value means React controls the input, defaultValue means the DOM controls it and React is just setting the starting point.
When uncontrolled wins
File inputs
File inputs are always uncontrolled. Full stop. You cannot set value on a file input programmatically for security reasons — the browser won't allow it. React can't control what file the user has selected.
Trying to control a file input with useState is fighting the platform. The ref approach is what the browser gives you.
One-shot forms
A login form. A search form. A quick feedback form. Forms where you only care about the value when the user submits — not on every keystroke.
No state. No onChange. No re-render on every keystroke. The form captures what it needs on submit. For a login form, this is all you need.
Performance-critical inputs
Every keystroke in a controlled input triggers a re-render. For most inputs this is imperceptible. But inside a component that's doing expensive work on every render — a canvas, a complex layout, a heavy computation — the constant re-renders add up.
An uncontrolled input with a ref reads the value only when needed and never triggers a render cycle from typing.
When controlled wins
Real-time validation
If you need to show validation errors as the user types — not just on submit — you need to know the current value on every change. That requires controlled.
You can't do this with a ref. You'd need to attach an onChange handler and read the value manually — at which point you're essentially rebuilding controlled inputs without the React integration.
Conditional rendering based on input value
If what's shown on screen depends on what's been typed — a search results panel, a character counter, a preview — you need the value in React state so React can render accordingly.
Programmatic control
If you need to reset a field, pre-populate it from an API response, or set its value from somewhere else in the UI — you need controlled. You can't set a ref value and have the input update correctly in all cases.
Or better — use the key prop pattern from Article #3 to remount the component entirely when the user changes, which resets all state without a useEffect.
The mixing trap
The most confusing bug in React forms happens when a component switches between controlled and uncontrolled — usually accidentally.
When initialValue is undefined, value starts as undefined, which makes the input uncontrolled. The first keystroke sets value to a string, switching it to controlled. React warns about this in the console: "A component is changing an uncontrolled input to be controlled."
The fix is always to initialise with a defined value:
defaultValue vs value is the API surface that tells you which mode you're in. value = controlled. defaultValue = uncontrolled. Never use both on the same input.
React Hook Form's trick
React Hook Form is the most widely-used form library in the React ecosystem, and it's built on uncontrolled inputs. It uses refs internally to read values — no useState, no re-renders on every keystroke.
But it gives you validation that feels like controlled inputs. How?
It only reads the ref values when validation is triggered — on submit, or on blur if you configure it. Then it sets error state (which does use React state) only when validation fails.
register returns a ref callback and an onChange handler — but the onChange doesn't update React state. It validates and updates error state only when needed. The input itself is uncontrolled.
This is why React Hook Form is fast with large forms. A form with fifty fields causes zero re-renders on every keystroke.
The defaultValue vs value decision
Here's the question to ask before writing any form input:
Will the current value of this input affect anything on screen before the user submits?
- Does it filter a list? → controlled
- Does it show a character count? → controlled
- Does it show inline validation errors? → controlled
- Does it drive conditional rendering? → controlled
- Does it only matter when the user submits? → uncontrolled
- Is it a file input? → uncontrolled
If none of the controlled cases apply, reach for defaultValue and a ref. Less state, fewer re-renders, simpler code.
The complete decision table
| Scenario | Controlled | Uncontrolled |
|---|---|---|
| Real-time validation | ✅ | — |
| Character counter | ✅ | — |
| Search-as-you-type | ✅ | — |
| Programmatic reset | ✅ | — |
| Conditional rendering on input | ✅ | — |
| One-shot form (login, search) | — | ✅ |
| File input | — | ✅ (required) |
| Large form, many fields | — | ✅ |
| Performance-critical context | — | ✅ |
| Integration with React Hook Form | — | ✅ (by default) |
The default of "controlled for everything" isn't wrong — controlled inputs work everywhere. But they carry the overhead of state and re-renders everywhere too. Knowing when the DOM can manage its own state — and giving it the chance to — is the decision most React developers never make explicitly.
Make it explicitly.
Up next: How to test custom hooks with React Testing Library — the renderHook pattern that makes hooks as testable as components.
Related: How to build a multi-step form in React with validation and persistence — where the controlled vs uncontrolled decision gets more interesting across multiple steps.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.

