Topic 5.1
Controlled Inputs
In one line
A controlled input gets its value from React state and reports every keystroke through onChange, so the state is the one source of truth and the field always shows exactly what the state says.
Think of it like this
A shop counter with a cashier. You don't write on the bill yourself. You tell the cashier what you want, the cashier writes it on the bill, and the bill shows only what the cashier wrote. If the cashier ignores you, the bill doesn't change. A controlled input works the same way: the keyboard asks for a change, your code (the cashier) decides, and the field shows only what your state says.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Input
- A form field the user can type into or choose from, like
<input>,<textarea>or<select>. - Controlled input
- A field whose value comes from React state through the
valueprop and changes only throughonChange. - value prop
- The prop that tells a field exactly what text to show. With it, React decides the field's content.
- onChange
- React's event for "the user edited this field". It fires on every keystroke, not only when you leave the field.
- Event object (e)
- The object React passes to your handler.
e.targetis the element the event happened on. - e.target.value
- The field's current text, always a string, even in a number field.
- Single source of truth
- Keeping a piece of data in exactly one place, so every part of the screen reads the same value.
- readOnly
- An attribute that lets people select and copy a field's text but not change it.
Step by step
01Make a piece of state for the field
The field needs somewhere to keep its text between renders. That is state, starting as an empty string "" (not undefined: you'll see why in Topic 5.3).
const [name, setName] = useState("");02Connect value and onChange
value={name} tells the field what to show. onChange receives the event and stores the new text. These two props together make the input controlled.
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}03Follow one keystroke round the loop
The user types A. The browser changes the field and fires input; React calls onChange; setName("A") asks for a render; React renders with name = "A" and makes sure the field shows A.
04Use the same state everywhere
Anything that depends on the text reads name. The greeting, the character count and the button's disabled state are worked out during render, so they always match the field.
<p>Hello, {name || "stranger"}!</p>
<p>{name.length} / 20 characters</p>
<button disabled={name.trim() === ""}>Save</button>05Transform what the user typed
The handler is your chance to clean the text before it becomes state. Whatever you store is what the field shows.
onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""))}Try it yourself
- 1
Add a live counter limit
In the first example, stop the bio at 140 characters by changing its handler to
setBio(e.target.value.slice(0, 140)). Predict: what happens when you paste a long paragraph? (Only the first 140 characters stay.) - 2
Shout everything
Change the Name handler to store
e.target.value.toUpperCase(). The field and the greeting both turn upper-case. Which line would you change to keep the field as typed but shout only in the greeting? - 3
Freeze the field
In the first example, change the Name handler to
() => {}. Type: nothing appears, and React prints no warning because there is anonChange. The field is controlled by state that never changes.
Code & diagrams
Type in the Name box and watch the console: every keystroke is one render. The greeting, the counter and the Save button all read the same state as the fields.
Preview
Console
Nothing logged yet.Expected console output
render: name = ""Type `save 10!` in the coupon box: it becomes `SAVE10`. Type letters in the phone box: nothing appears, because the handler throws them away before they reach state.
Preview
Console
Nothing logged yet.React warns about the first field only. The second one says `readOnly`, so React knows the missing `onChange` is on purpose.
Preview
Console
Nothing logged yet.Expected console output
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Give a field a value but no onChange
In the first example, delete onChange={(e) => setName(e.target.value)} from the Name input and try to type.
Break #2
Start the state as null
Change the Name state to useState(null) and fix the render line to name ?? "" where needed.
Myth vs fact
Myth
onChange fires when the user leaves the field, like the HTML change event.
Fact
React's onChange fires on every edit. React attaches it to the browser's input event for text fields. If you want "left the field", use onBlur (Topic 5.6).
Myth
Re-rendering on every keystroke makes controlled inputs slow.
Fact
For normal forms one render per key is cheap. It only matters when each keystroke re-renders something very large, and Phase 16 shows fixes such as moving state down and useDeferredValue.
Myth
e.target.value of a number field is a number.
Fact
It is always a string. Convert it yourself when you need a number (Topic 5.2).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React tracks a field's value through a hidden "value tracker" that wraps the DOM
valuesetter. This is why code that setsinput.value = "x"directly and then fires aninputevent does not triggeronChange: React thinks nothing changed. Test tools such as Testing Library'suser-eventand Hectal'stype()helper call the prototype's setter to get around it. - ▸
After each change event React "restores" controlled fields: if the state you set differs from what is in the DOM, React writes the state back. When your handler transforms the text (upper-casing, removing characters), the written value differs from what the browser had, and the caret can jump to the end. For masks that edit the middle of the text, save and restore
selectionStartwith a ref (Phase 8). - ▸
Typing in languages that use an input method editor (IME), such as Japanese or Chinese, sends several
inputevents while a word is being composed. Controlled inputs handle this, but transforming the text mid-composition (for example upper-casing) can break it; checke.nativeEvent.isComposingif you filter text.
Remember this
- 1
An input is a form field the user can type into, such as
<input>or<textarea>. In plain HTML the browser stores what the user typed inside the field itself. A controlled input is one where React stores it instead: you pass the field avaluefrom state and anonChangehandler that updates that state.<input value={name} onChange={(e) => setName(e.target.value)} />is the whole pattern. - 2
Here is the loop on every keystroke. The user presses a key; the browser puts the new text in the field and fires an event; React calls your
onChangewith an event object whosee.targetis the field, soe.target.valueis the new text (always a string). You callsetName, React re-renders, and writesvalue={name}back into the field. If you don't update the state, React puts the old value back: the key press is undone. That is what "controlled" means. - 3
Because the value lives in state, there is one source of truth. Anything else on the page that needs the text (a greeting, a character counter, a search filter, a disabled Save button) reads the same state variable, so they can never disagree with the field. This is UI = f(state) from Topic 0.1, applied to forms.
- 4
React's
onChangeis not the browser'schangeevent. The browser'schangefires for text fields only when you leave the field; React'sonChangefires on every edit (React listens to the browser'sinputevent under the hood). So in React,onChangeis the event for "the text changed". - 5
A
<textarea>(a multi-line text box) is controlled exactly like an input:<textarea value={bio} onChange={...} />. In HTML you put a textarea's text between its tags; in React you use thevalueprop instead, and React warns if you use children. - 6
Since your handler decides what goes into state, you can transform input as you type: make a coupon code upper-case with
e.target.value.toUpperCase(), keep only digits with.replace(/\D/g, ""), or cut text at a maximum length with.slice(0, 10). The field then shows the transformed text, because the field shows the state. - 7
If you give a field a
valuebut noonChange, React prints a warning:You provided a \value\prop to a form field without an \onChange\handler. This will render a read-only field.The field really is stuck, because React keeps putting the same value back. Fix it by addingonChange, by usingdefaultValue(an uncontrolled field, Topic 5.3), or by addingreadOnlyif you meant it to be read-only.
Explain it without notes
What makes an input controlled? Walk through what happens on one keystroke.
Why does React warn when you pass value without onChange, and what are the three fixes?
How is React's onChange different from the DOM change event?
What does "single source of truth" mean for a form field, and why does it help?
Practice
Make a controlled <input> for a username that removes spaces as you type and shows @username below it.
Make a controlled <textarea> with a counter N characters left that starts at 100 and stops the user typing past 100.
Trade-offs
- ↔
Controlled inputs make every value available on every render (live previews, instant validation, dependent fields), at the cost of one render per keystroke and a little more code per field.
- ↔
Transforming text in
onChangeis powerful but can surprise users (the caret jumps, pasted text changes). For heavy masks, consider validating instead of rewriting, or a small masking library.
Done when you can
I can write a controlled input and textarea with
valueandonChange.I can explain the keystroke → onChange → state → render loop.
I can show the same state in several places on the page.
I can transform text as the user types.
I can explain and fix the read-only field warning.