Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~22 min· topic 1 of 7

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 value prop and changes only through onChange.
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.target is 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).

NameField.jsxwhole filejsx
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.

NameField.jsxwhole filejsx
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.

Follow one keystroke round the loopdiagram
Rendering diagram…

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.

NameField.jsxwhole filejsx
<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.

Coupon.jsxwhole filejsx
onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""))}

Try it yourself

  1. 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. 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. 3

    Freeze the field

    In the first example, change the Name handler to () => {}. Type: nothing appears, and React prints no warning because there is an onChange. The field is controlled by state that never changes.

Code & diagrams

A controlled input and a textarea New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

render: name = ""
Transforming input as you type New tab

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

Press Run to see the page.

Console

Nothing logged yet.
A value with no onChange is read-only New tab

React warns about the first field only. The second one says `readOnly`, so React knows the missing `onChange` is on purpose.

Preview

Press Run to see the page.

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`.
Who owns the text?diagram
Rendering diagram…

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.

terminal
$ Console
── what you'll see ──
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 #2

Start the state as null

Change the Name state to useState(null) and fix the render line to name ?? "" where needed.

terminal
$ Console
── what you'll see ──
`value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.

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 value setter. This is why code that sets input.value = "x" directly and then fires an input event does not trigger onChange: React thinks nothing changed. Test tools such as Testing Library's user-event and Hectal's type() 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 selectionStart with a ref (Phase 8).

  • ▸

    Typing in languages that use an input method editor (IME), such as Japanese or Chinese, sends several input events while a word is being composed. Controlled inputs handle this, but transforming the text mid-composition (for example upper-casing) can break it; check e.nativeEvent.isComposing if you filter text.

Remember this

  1. 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 a value from state and an onChange handler that updates that state. <input value={name} onChange={(e) => setName(e.target.value)} /> is the whole pattern.

  2. 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 onChange with an event object whose e.target is the field, so e.target.value is the new text (always a string). You call setName, React re-renders, and writes value={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. 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. 4

    React's onChange is not the browser's change event. The browser's change fires for text fields only when you leave the field; React's onChange fires on every edit (React listens to the browser's input event under the hood). So in React, onChange is the event for "the text changed".

  5. 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 the value prop instead, and React warns if you use children.

  6. 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. 7

    If you give a field a value but no onChange, 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 adding onChange, by using defaultValue (an uncontrolled field, Topic 5.3), or by adding readOnly if you meant it to be read-only.

Explain it without notes

01

What makes an input controlled? Walk through what happens on one keystroke.

02

Why does React warn when you pass value without onChange, and what are the three fixes?

03

How is React's onChange different from the DOM change event?

04

What does "single source of truth" mean for a form field, and why does it help?

Practice

01

Make a controlled <input> for a username that removes spaces as you type and shows @username below it.

02

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 onChange is 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 value and onChange.

  • 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.