Controlled vs uncontrolled inputs
- 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
A controlled input — value + onChange driven by state
FlagA 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.
An uncontrolled input — defaultValue + a ref, DOM owns the value
FlagAn 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.
Data that arrives late — the warning, and the field that stays empty
FlagAn 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?
FlagYou render <input value={query} /> with no onChange. What happens when the user types?
When is an uncontrolled input (defaultValue + a ref) a reasonable choice?
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?
Fill in the blank
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)} />;
}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".Challenge
Fix the search box that won't type
FlagSearchList 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