codejump

AcademyReact

Controlled vs Uncontrolled Components

Whether React state or the DOM itself owns an input value — and the one mistake that flips a field between the two mid-render.

3 min readUpdated

3 exercises

Waiting at the end of this page.

Two ways to own an input's value

Every form field in the DOM already has its own internal value — that is how <input> worked long before React existed. The question a controlled-vs-uncontrolled decision answers is: does React state mirror that value and drive it, or does the DOM just keep its own value and React reads it only when asked?

Controlled: React state is the source of truth

JSX
function ControlledInput() {
  const [value, setValue] = useState('');
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

Every keystroke fires onChange, which updates state, which re-renders the input with that state as its value. The DOM node never gets to decide what it displays — it displays whatever value says, and typing only appears to work because onChange keeps state in lockstep with every keystroke. Because the value lives in state, you can inspect it, transform it, or reject it on every change:

JSX
<input
  value={value}
  onChange={(e) => setValue(e.target.value.toUpperCase())}
/>
// the field displays what you typed, forced to uppercase,
// on the very keystroke you typed it

Uncontrolled: the DOM is the source of truth

JSX
function UncontrolledInput() {
  const inputRef = useRef(null);

  function handleSubmit() {
    console.log(inputRef.current.value); // read only when needed
  }

  return <input ref={inputRef} defaultValue="" />;
}

No value prop, no onChange driving every keystroke — the input manages its own value the way it always has, and you reach in with a ref only at the moment you actually need it (on submit, typically). defaultValue sets the starting value and nothing more; it is not kept in sync afterward.

Picking one

Use controlled whenUse uncontrolled when
You validate or format as the user typesThe value is only needed once, at submit
Another part of the UI must react to this value live (character counter, disabled submit button)Re-rendering on every keystroke is measurably wasteful (very large forms)
You need to programmatically set the value from elsewhereYou are integrating a non-React widget that already manages its own DOM state

File inputs are always uncontrolled — the browser refuses to let JavaScript set <input type="file">'s value for security reasons, so inputRef.current.files is the only way in regardless of preference.

The bug: flipping controlled mid-render

React decides whether an input is controlled by whether its value prop is undefined on first render. Initialize state to undefined and pass it straight through, and the field starts life uncontrolled — then the moment state is first set to a real string, it becomes controlled, and React logs a warning: a component is changing an uncontrolled input to be controlled.

JSX
// BAD — starts as undefined, becomes a string later
const [name, setName] = useState();
<input value={name} onChange={(e) => setName(e.target.value)} />
JSX
// GOOD — always a string, controlled from the very first render
const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />

The fix is never "pick undefined on purpose to start uncontrolled" — it is to decide up front which one this field is, and initialize state so the value prop is the same kind of thing (always a string, or always absent) on every render, including the first.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.