Controlled inputs

Drives a form field entirely from React state by pairing value with an onChange handler.

Signature
const [value, setValue] = useState('');
<input value={value} onChange={e => setValue(e.target.value)} />

Parameters

ParameterTypeDescription
$valuestringThe current text, taken from state. On <input type="checkbox"> and type="radio" the equivalent prop is checked, a boolean. On <select multiple> it is an array of strings. Passing it is what makes the field controlled.
$onChange(event: Event) => voidRequired alongside value. Must synchronously update the state backing the value prop, usually with e.target.value — or e.target.checked for a checkbox. Fires on every keystroke.
$readOnlyoptionalboolean= falsePass true instead of onChange when a field is deliberately fixed. This silences React's error about a value prop with no change handler.

Return value

Not a callable API. A controlled field renders exactly what state says: React writes the value prop into the DOM on every render, and the onChange handler writes the user's edit back into state.

Errors

React logs an error when value is passed without onChange and without readOnly, and when a field's value changes from undefined or null to a string, which flips it from uncontrolled to controlled.

A controlled field has no memory of its own. React renders whatever value says, the user's keystroke fires onChange, that handler updates state, and the re-render puts the new text back on screen. State is the single source of truth, so the value you read in a render is always exactly what the user sees.

When to reach for it

Control a field whenever something else depends on it while the user types: a live preview, a filtered list, a submit button that enables at three characters, an input whose text you normalise as it is entered, or two fields that must stay in sync. Also control fields whose value has to be set from elsewhere — a Reset button, a Fill from profile shortcut, a value arriving from a parent.

What trips people up

  • Start state at '', never undefined or null. A value that is undefined on the first render and a string later flips the field from uncontrolled to controlled and can drop focus.
  • A value with no onChange makes a field that cannot be typed in — the single most reported React bug.
  • Checkboxes use checked and e.target.checked, not value.
  • Every keystroke re-renders the component holding the state, so keep that component small in a large form.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Filter a list live as the user types a search term
  • Enable a submit button only once a field passes validation
  • Normalise input as it is entered, such as forcing a coupon code to uppercase

Frequently asked questions

What is the difference between a controlled and an uncontrolled input?

A controlled input takes its value from React state and cannot change without a state update; an uncontrolled input keeps its own value in the DOM and you read it later through a ref or FormData. Control a field when other UI must react while the user types; leave it uncontrolled when you only read it on submit.

Why does my controlled input lose focus while typing?

Almost always because the component rendering the input is being recreated — defined inside another component's body, or given a changing key — so React unmounts the old DOM node and mounts a new one. Move the component definition to the module's top level and keep keys stable across renders.

How do I clear a controlled input?

Set its state back to an empty string: setValue(''). Never assign to inputRef.current.value on a controlled field — React will overwrite it on the next render, since the value prop is the source of truth.

More examples

Several fields in one state object

One handler serves three fields. It keys off e.target.name to decide which property to replace and off e.target.type to read checked rather than value for the checkbox. The <pre> block mirrors state exactly, so editing the email field immediately rewrites the email line in the JSON. Note the functional update form: without it, two changes dispatched in the same tick would each spread a stale copy of the object and one would be lost.

Example · jsx
import { useState } from "react";

export default function ProfileForm() {
  const [form, setForm] = useState({
    name: "Ada",
    email: "[email protected]",
    newsletter: true,
  });

  function handleChange(e) {
    const { name, type, value, checked } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value,
    }));
  }

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <label>
        <input
          name="newsletter"
          type="checkbox"
          checked={form.newsletter}
          onChange={handleChange}
        />{" "}
        Newsletter
      </label>
      <pre>{JSON.stringify(form, null, 2)}</pre>
    </form>
  );
}

Transforming the value on the way into state

Typing `ab-12` puts AB12 in the field: lowercase is upcased, the dash is stripped, and anything past eight characters is dropped, all before the value reaches state. Because the field renders from state rather than from what was typed, the cleanup is visible immediately and there is no way for the DOM to hold a value the app has not approved. The Apply button stays disabled until the code is exactly eight characters — this instant coupling of field to other UI is the reason to control a field at all.

Example · jsx
import { useState } from "react";

export default function CouponField() {
  const [code, setCode] = useState("");

  function handleChange(e) {
    const cleaned = e.target.value
      .toUpperCase()
      .replace(/[^A-Z0-9]/g, "")
      .slice(0, 8);
    setCode(cleaned);
  }

  const valid = code.length === 8;

  return (
    <p>
      <label>
        Coupon: <input value={code} onChange={handleChange} placeholder="ABCD1234" />
      </label>{" "}
      <button type="button" disabled={!valid}>
        Apply
      </button>
      <br />
      {valid ? "Looks valid." : `${8 - code.length} character(s) to go.`}
    </p>
  );
}

Discussion

  • Be the first to comment on this lesson.