Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

Controlled vs uncontrolled inputs in React: who owns the value, the render-count difference, and that warning

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 t

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

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.