Runnable lesson · press Try it on the examples to edit and run them here.
A controlled input is an input whose value is held in React state. The input shows what the state says, and every keystroke updates the state.
The pattern
function App() {
const [name, setName] = React.useState("");
return (
<div>
<input
aria-label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello, {name}</p>
</div>
);
}
Two props work together:
value={name}makes the input display whatevernameis.onChangeupdatesnamewhen the user types.
Typing sets a loop in motion: you press a key, onChange runs, state changes, React re-renders, and the input shows the new text. React is in charge of the input, not the browser.
Why bother?
Because state is the single source of truth, you can do things that are hard otherwise:
- Validate or format as the user types (force upper case, remove spaces).
- Show the value elsewhere: a live preview, a character counter.
- Reset or change the value from code: clear a form after submit.
- Disable the submit button until the data is valid.
function App() {
const [text, setText] = React.useState("");
const max = 20;
return (
<div>
<input
aria-label="Message"
value={text}
maxLength={max}
onChange={(e) => setText(e.target.value.replace(/\s{2,}/g, " "))}
/>
<p>{text.length}/{max}</p>
<button onClick={() => setText("")} disabled={!text}>Clear</button>
</div>
);
}
What if you forget onChange?
If you set value but not onChange, the input becomes read-only: it always shows the state’s value and typing changes nothing. React warns you in the console. Use defaultValue for a field with an initial value that the browser should manage.
function App() {
const [text] = React.useState("Frozen");
return (
<div>
<input aria-label="Frozen input" value={text} />
<p>Value in state: {text}</p>
</div>
);
}
Uncontrolled inputs
An uncontrolled input keeps its own value in the DOM. You read it only when you need it, using a ref or the form data:
function App() {
const inputRef = React.useRef(null);
const [result, setResult] = React.useState("");
return (
<div>
<p>Uncontrolled</p>
<input aria-label="Name" ref={inputRef} defaultValue="" />
<button onClick={() => setResult("Value read: " + inputRef.current.value)}>Read value</button>
<p>{result}</p>
</div>
);
}
| Controlled | Uncontrolled | |
|---|---|---|
| Value lives in | React state | The DOM |
| Live validation, formatting, preview | Easy | Hard |
| Code amount | A bit more | Less |
| Good for | Most forms with feedback | Simple forms, file inputs, integrating non-React code |
File inputs (<input type="file">) are always uncontrolled, because only the user can set a file.
Common problems
- “A component is changing an uncontrolled input to be controlled.” The state started as
undefined(no value) and later became a string. Start with an empty string:useState(""). - The cursor jumps to the end when you change the value in strange ways in
onChange. Transform carefully, or keep the user’s text and format only when displaying. - Numbers.
e.target.valueis always a string. Convert withNumber(value)when you need a number, and remember that an empty field gives0; check for the empty string first. - Textareas and selects work the same: use
valueandonChange. In React,<textarea value={text} />takes the text as a prop, not as children.
Key points
- A controlled input has
valuefrom state andonChangethat updates it. - State becomes the single source of truth, which makes validation and resetting simple.
- Start with
"", notundefined, and remembere.target.valueis a string.