DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

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 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)} />
Enter fullscreen mode Exit fullscreen mode

The value lives in state. Every keystroke fires onChangesetName → 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
Enter fullscreen mode Exit fullscreen mode

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 🐞
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
hoseinmdev profile image
Hosein Mahmoudi

it was useful thanks

Collapse
 
topstar_ai profile image
Luis Cruz

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?