ESC

Type to search the knowledge base.

Controlled vs Uncontrolled Inputs

React form inputs as source of truth — controlled value/onChange, uncontrolled defaultValue/ref, hybrid pitfalls, and when each wins.

beginner5 min read
  • react
  • forms
  • controlled
  • uncontrolled
  • inputs

In the DOM, an <input> owns its string. In React you choose who is the source of truth for that string while the component is mounted:

  • Controlled — React state (or props) drives value; every keystroke updates state; the DOM is a mirror.
  • Uncontrolled — the DOM keeps the value; you read it on demand via a ref (often on submit).

Neither is universally “more React.” Controlled wins when the UI must react to every change. Uncontrolled wins when you only care at submit time and want less re-render traffic.

Official framing: react.dev — sharing state / forms patterns.

Controlled: state is truth

function SearchBox() {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      aria-label="Search"
    />
  );
}

Data flow:

  1. User types → onChange
  2. setQuery schedules re-render
  3. Input receives the new value

You can always derive UI from query: clear buttons, character counts, debounced fetch, validation messages.

function PriceField() {
  const [raw, setRaw] = useState('');
  const invalid = raw !== '' && Number.isNaN(Number(raw));

  return (
    <>
      <input
        inputMode="decimal"
        value={raw}
        onChange={(e) => setRaw(e.target.value)}
        aria-invalid={invalid}
      />
      {invalid && <p role="alert">Enter a number</p>}
    </>
  );
}

Controlling other form controls

Control Controlled props
text/email/search value + onChange
textarea value + onChange
select value + onChange
checkbox checked + onChange
radio group checked={value === option} + onChange
function RememberMe() {
  const [on, setOn] = useState(false);
  return (
    <label>
      <input
        type="checkbox"
        checked={on}
        onChange={(e) => setOn(e.target.checked)}
      />
      Remember me
    </label>
  );
}

Uncontrolled: DOM is truth

function NameForm({ onSubmit }) {
  const inputRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    onSubmit(inputRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} name="name" defaultValue="" aria-label="Name" />
      <button type="submit">Save</button>
    </form>
  );
}

Use defaultValue / defaultChecked for initial DOM state — not value without onChange.

Uncontrolled fits:

  • Simple “submit the form” screens
  • File inputs (always uncontrolled in practice)
  • Integrating non-React widgets that own the DOM
  • Performance-sensitive huge forms where per-keystroke React work is measurable and unnecessary
function AvatarPicker({ onFile }) {
  return (
    <input
      type="file"
      accept="image/*"
      onChange={(e) => onFile(e.target.files?.[0] ?? null)}
    />
  );
}

File inputs cannot be fully controlled with a synthetic string value for security reasons.

The hybrid bug: value without onChange

// Locked input — React sets value, user cannot change it
<input value={name} readOnly /> // intentional lock

// Accidental lock — missing onChange
<input value={name} /> // warning in dev; typing does nothing useful

If you pass value, you must either update it from onChange or mark the field read-only intentionally.

Switching from uncontrolled to controlled mid-life (or the reverse) also warns:

// First render: undefined value → uncontrolled
// Later: value="" → controlled
<input value={maybeUndefined} onChange={…} />
// Prefer value={maybeUndefined ?? ''} for always-controlled

Keep a field in one mode for its mounted lifetime.

When controlled is worth the re-renders

  • Instant validation / formatting (phone masks, uppercase codes)
  • Disable submit until valid
  • Multi-field constraints (end date ≥ start date)
  • Syncing URL query params with the field
  • Dependent fields (country → state list)
  • Debounced server search as you type
function TagInput() {
  const [value, setValue] = useState('');
  const normalized = value.trim().toLowerCase();

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      onBlur={() => setValue(normalized)}
    />
  );
}

Lifting state vs local controlled state

Controlled does not mean “everything lives in Redux.” It means the input’s value is driven by React state somewhere:

  • Local useState in the field component
  • Parent form state
  • Form library store

Lift only as far as siblings need to read the value (Props vs State, Lifting State Up).

Form libraries

Libraries (React Hook Form, Formik, etc.) often use uncontrolled or ref-based registration for performance, then surface controlled-like APIs for specific fields. Knowing the native distinction helps you debug when a library field “won’t type” (double control) or “won’t reset” (ref vs state).

Reset patterns

Controlled:

setName('');

Uncontrolled:

formRef.current.reset();
// or remount:
<Form key={formVersion} />

Remount via key resets uncontrolled fields cleanly when switching records.

Footguns

  1. value without onChange.
  2. Flipping undefined ↔ string (uncontrolled ↔ controlled warning).
  3. Controlling with state updated asynchronously only — caret jumps or lag if you fight the input.
  4. Formatting on every keystroke incorrectly — cursor jumps to end; use careful selection restore or format on blur.
  5. Using defaultValue and value together — pick one mode.
  6. Reading ref.current.value in render — refs don’t trigger re-render; UI won’t update.

Interview angle

Prompt: “Controlled vs uncontrolled components?”

Strong answer: “Controlled inputs take value from React state and update through onChange, so React is the source of truth every render. Uncontrolled inputs keep DOM state, initialized with defaultValue, read via refs usually on submit. Controlled is better for dynamic validation and dependent UI; uncontrolled is lighter when you only need values at submit. Don’t mix modes on one field, and always handle ok-style consistency for file inputs as uncontrolled.”

Live task: convert a controlled search box to uncontrolled submit form and name one feature you lose (live filtering).

Further reading

Related guides