Every React <input> is either controlled or uncontrolled, and the entire distinction comes down to one question: who owns the value — React, or the DOM? Get it straight once and a whole category of form bugs (including that warning) disappears. So I built a side-by-side where you type into both and watch the render counters.
▶ Live demo: https://controlled-vs-uncontrolled-kappa.vercel.app/
Source: https://github.com/dev48v/controlled-vs-uncontrolled
Controlled — React owns the value
const [name, setName] = useState("");
<input value={name} onChange={(e) => setName(e.target.value)} />
The value lives in state. Every keystroke fires onChange → setName → a re-render. In the demo, typing "abcd" takes the render counter from 1 to 5. That sounds wasteful, but it's the point: React always holds the current value, so you can validate, format, or transform it as the user types (the demo has an "UPPERCASE as you type" toggle that works precisely because React controls what goes back into the input).
Uncontrolled — the DOM owns the value
const ref = useRef(null);
<input defaultValue="" ref={ref} />
// later: ref.current.value
The value lives in the DOM node. Typing does not re-render React — in the demo the uncontrolled counter stays at 1 no matter how much you type. You read the value with a ref when you actually need it (usually on submit). Note defaultValue, not value: it sets the initial value once, then hands control to the DOM.
The warning everyone eventually hits
Warning: A component is changing an uncontrolled input to be controlled. (or the reverse)
This means an input's value went from undefined/null to a real string (or back) between renders. React decides controlled-vs-uncontrolled on the first render based on whether value is defined, and it can't switch afterward. The usual cause:
const [name, setName] = useState(); // undefined on first render
<input value={name} onChange={...} /> // uncontrolled → then controlled 🐞
Or value={user.name} where user is null until a fetch resolves. The fix is to always pass a defined value:
const [name, setName] = useState(""); // defined from the start
<input value={name ?? ""} onChange={...} /> // stays controlled
So which should you use?
Controlled when you need the value during typing:
- live validation / inline errors
- character counters, input masks, formatting (phone, currency)
- enabling/disabling the submit button as they type
- one field that depends on another
Uncontrolled when you don't:
- a simple form you only read on submit
- integrating a non-React widget, or migrating legacy code
-
<input type="file">— file inputs are always uncontrolled; you can't set their value programmatically
Default to controlled; it's the more predictable "single source of truth" model. Reach for uncontrolled when re-rendering on every keystroke is a measurable problem — which is also why form libraries like React Hook Form lean on refs (uncontrolled) under the hood and only re-render when they have to.
Type in both panels, flip the uppercase toggle, and read the uncontrolled value via its ref. If it made the distinction click, a star helps others find it: https://github.com/dev48v/controlled-vs-uncontrolled
Top comments (2)
it was useful thanks
I particularly appreciate how you highlighted the importance of understanding who owns the value in React inputs, and how this distinction can help avoid a whole category of form bugs. The live demo you provided really drives home the difference in render counts between controlled and uncontrolled inputs. One scenario where I've found uncontrolled inputs to be particularly useful is when integrating third-party libraries that manage their own state, as it can simplify the integration process and reduce unnecessary re-renders. Have you encountered any situations where the choice between controlled and uncontrolled inputs had a significant impact on performance?