React Hooks & Rendering
React Hooks & Rendering

Controlled vs uncontrolled inputs

Full · ~25 min
After this unit you can
  • Wire a controlled input by pairing value from state with an onChange that writes e.target.value
  • Predict what an input with value but no onChange does: a frozen field plus a React warning
  • Choose an uncontrolled input (defaultValue plus a ref) when you only need the value on submit
  • Spot the uncontrolled-to-controlled warning and fix it by keeping value defined from the first render
  • Show data that loads after the first render in an editable field: render the form once it's loaded, or remount it with a key

Read

Simple

A controlled input — value + onChange driven by state

Flag

A controlled input has its value owned by React state. You pass the current value down with value, and every keystroke flows back up through onChange:

function SearchBox() {
  const [query, setQuery] = useState("");
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

The two props are a pair. value makes React the single source of truth for what's shown; onChange is how the input tells state to update. Leave out onChange and the field becomes read-only — typing does nothing, and React warns you.

Rule of thumb: if you need to read or react to the value as the user types (filtering a list, validating, enabling a button), make it controlled.

Medium

An uncontrolled input — defaultValue + a ref, DOM owns the value

Flag

An uncontrolled input lets the DOM keep the value. You seed the initial value with defaultValue (not value) and read the current value from a ref when you need it — usually on submit:

function ProfileForm() {
  const nameRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    console.log(nameRef.current.value); // read on demand
  }

  return (
    <form onSubmit={handleSubmit}>
      <input defaultValue="Ada" ref={nameRef} />
      <button>Save</button>
    </form>
  );
}

Because there's no state update per keystroke, the component doesn't re-render as the user types. That's fine — and even preferable — when you only care about the value at a single moment (form submit) and don't need to react to each change.

When to reach for uncontrolled:

  • You just need the final value on submit.
  • You're integrating a non-React widget or a file input (<input type="file"> is always uncontrolled — code can clear its value but never set it).
  • Per-keystroke re-renders would be wasteful and you gain nothing from them.

Note: defaultValue is only the starting value — react.dev: it "does not control what the value should be right now." Once the field holds a value (seeded on mount, or typed by the user), don't count on a new defaultValue to replace it; the DOM is in charge now. To load different data into an uncontrolled field, remount it with a new key.

Complex

Data that arrives late — the warning, and the field that stays empty

Flag

An input is controlled when it gets a value, and value={undefined} (or null) counts as no value at all. An input mustn't switch between the two during its life. If value starts undefined and later becomes a string, React logs:

A component is changing an uncontrolled input to be controlled.

The classic cause is binding value straight to data that hasn't loaded yet:

function NameField({ user, onNameChange }) {
  // 🐛 user is {} until the fetch resolves: value={undefined}, then value="Ada"
  return <input value={user.name} onChange={(e) => onNameChange(e.target.value)} />;
}

The fix is to keep value a string from the first render (react.dev's own advice):

<input value={user.name ?? ""} onChange={(e) => onNameChange(e.target.value)} />

The mirror-image warning — "changing a controlled input to be uncontrolled" — happens when value goes from a string back to undefined/null (resetting state to undefined, say). Same rule fixes both: pick controlled or uncontrolled and keep value consistently defined (or consistently absent) for the input's whole life.

Quiz

What makes an <input> a controlled component in React?

Flag

You render <input value={query} /> with no onChange. What happens when the user types?

Flag

When is an uncontrolled input (defaultValue + a ref) a reasonable choice?

Flag

NameForm seeds const [name, setName] = useState(initialName) and binds the input to name. The parent renders <NameForm initialName={user?.name ?? ""} /> while user is loading, and again once it arrives. What does the field show, and what's the fix?

Flag

Fill in the blank

Wire up a controlled text input bound to the query state.

Flag
<input value={query} ={(e) => setQuery(e.target.)} />

user is {} until the fetch resolves. Keep this input controlled from the very first render.

Flag
<input value={user.name  ""} onChange={(e) => onNameChange(e.target.value)} />

Predict the output

function NameField({ user, onNameChange }) {
  // user is {} until the fetch resolves, then { name: "Ada" }
  return <input value={user.name} onChange={(e) => onNameChange(e.target.value)} />;
}
Flag
function NameField({ user }) {
  // user is {} until the fetch resolves, then { name: "Ada" }
  const [name, setName] = useState(user.name);
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

// The fetch resolves. Then the user types "x".
Flag

Challenge

Fix the search box that won't type

Flag

SearchList renders a filter input above a list of fruit. The input sets its value from state — but there's no onChange, so typing never updates the state. The field looks frozen, and the list never filters.

This is the classic half-wired controlled input: value in, but no way for the input to push changes back to state.

Your task

Edit SearchList.tsx so the tests pass:

  • Typing in the search box updates the input's displayed value
  • The list filters to items matching the typed text (case-insensitive)
  • Clearing the box shows all items again

Loading the in-browser editor…

Further reading