An interactive, side-by-side demo of the two kinds of React inputs, with live render counters that make the difference obvious.
▶ Live: https://controlled-vs-uncontrolled-kappa.vercel.app/
Every React <input> is one of two things, and the whole distinction is: who 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. Because React always has the current value, you can validate, format, or transform it live (the demo has an "UPPERCASE as you type" toggle). Type "abcd" and the render counter goes 1 → 5.
const ref = useRef(null);
<input defaultValue="" ref={ref} />
// read on demand: ref.current.valueThe value lives in the DOM node. Typing does not re-render React — the counter stays put no matter how much you type. You read the value with a ref when you actually need it (e.g. on submit). defaultValue sets the initial value only; after that the DOM is in charge.
Warning: A component is changing a controlled input to be uncontrolled.
It fires when an input's value flips between a real string and undefined/null across renders — usually because state started undefined (useState() with no argument, or value={data?.name} before data loads). Fix: always pass a defined value.
const [name, setName] = useState(""); // not useState()
<input value={name ?? ""} onChange={...} />- Controlled — when you need the value while typing: live validation, character count, formatting, enabling/disabling submit, fields that depend on each other. Default to this.
- Uncontrolled — simple forms you read on submit, integrating a non-React widget, or
<input type="file">(always uncontrolled). Form libraries like React Hook Form use refs (uncontrolled) under the hood to avoid re-rendering on every keystroke.
React 19 · TypeScript · Vite.
npm install
npm run devMIT © 2026 dev48v — dev48v.infy.uk