DevgainsDevgainsDevgains
All articles

Controlled vs Uncontrolled Inputs: The React Form Decision You Keep Getting Wrong

·4 min read
Controlled vs Uncontrolled Inputs: The React Form Decision You Keep Getting Wrong

Photo: Unsplash

Almost every React form bug I've debugged comes down to a single confusion: is this input controlled or uncontrolled? Teams mix the two without realizing it, and the result is the classic symptom list — an input you can't type in, a warning in the console about switching between controlled and uncontrolled, or a form that re-renders the entire page on every keystroke.

The fix isn't a library. It's understanding who owns the value.

The two models

In a controlled input, React state is the single source of truth. The input's value is driven by state, and an onChange handler writes every keystroke back into state. The DOM holds nothing authoritative.

function ControlledName() {
  const [name, setName] = useState("");
  return (
    <input value={name} onChange={(e) => setName(e.target.value)} />
  );
}

In an uncontrolled input, the DOM owns the value. React doesn't track it on every keystroke; you read it when you need it, usually via a ref. The React docs on refs to manipulate the DOM cover the mechanics.

function UncontrolledName() {
  const inputRef = useRef(null);
  function handleSubmit() {
    console.log(inputRef.current.value); // read on demand
  }
  return <input ref={inputRef} defaultValue="" />;
}

Note defaultValue, not value. That one prop is the tell: value makes it controlled, defaultValue makes it uncontrolled. The official reference for <input> spells out both modes.

The bug everyone ships

The console warning "A component is changing an uncontrolled input to be controlled" means your value prop started as undefined and later became a string. React saw the input flip ownership mid-life.

// Bug: user?.name is undefined on first render, then a string after fetch.
<input value={user?.name} onChange={...} />
 
// Fix: guarantee a defined value from the first render.
<input value={user?.name ?? ""} onChange={...} />

A controlled input's value must never be undefined or null. Default it to "" so the input is controlled from the very first render to the last.

When to use which

Reach for controlled when the value drives the UI as the user types:

  • Live validation and inline error messages
  • Disabling a submit button until the form is valid
  • Formatting on the fly (phone numbers, currency)
  • One input's value affecting another field

Reach for uncontrolled when you only need the value at submit time:

  • Simple forms where nothing reacts to keystrokes
  • Wrapping non-React widgets or <input type="file"> (which is always uncontrolled — you can't set its value programmatically for security reasons)
  • Squeezing out re-renders on a very large form

The performance angle

A controlled input re-renders its component on every keystroke. For one field that's nothing. For a 40-field form where each keystroke re-renders the whole form, it's the difference between smooth and janky — and it shows up directly in your interaction latency.

You have three good options:

  1. Isolate state. Put each controlled field in a small component so a keystroke only re-renders that field, not the form.
  2. Go uncontrolled and read values at submit via refs or FormData.
  3. Use a form library like React Hook Form, which is uncontrolled by default and subscribes components to only the fields they care about.

The modern platform also gives you FormData for free, which pairs beautifully with uncontrolled forms and Server Actions:

function handleSubmit(e) {
  e.preventDefault();
  const data = new FormData(e.currentTarget);
  const name = data.get("name");      // no per-keystroke state at all
}

A simple decision rule

Ask one question: does anything need to react to this value while the user is typing?

  • Yes → controlled, and isolate the state so re-renders stay local.
  • No → uncontrolled, read it at submit with a ref or FormData.

Pick deliberately per field. The bugs come from picking by accident.

Takeaways

  • Controlled = React state owns the value (value + onChange); uncontrolled = the DOM owns it (defaultValue + ref).
  • Always default a controlled value to "" to avoid the controlled/uncontrolled warning.
  • <input type="file"> is always uncontrolled.
  • Controlled inputs re-render on every keystroke — isolate state, go uncontrolled, or use a form library for big forms.
  • Decide per field by asking whether anything reacts to the value mid-typing.
4 min read

Read next