Skip to content
React rc state-events 4 min read

Controlled Inputs

A controlled input is a form element whose displayed value is driven entirely by React state rather than by the DOM. You bind the input’s value to a state variable and update that state in an onChange handler, so React becomes the single source of truth for what the user sees. This pattern makes input data trivial to read, validate, transform, and reset—at the cost of a small amount of wiring. It is the foundation that the dedicated Forms section builds on.

How a controlled input works

The contract is two-way but deliberate: the input renders whatever is in state, and every keystroke flows back into state through onChange. React then re-renders with the new value, which the input displays. Without that loop, the field would appear frozen.

import { useState } from "react";

function NameField() {
  const [name, setName] = useState("");

  return (
    <label>
      Name:{" "}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </label>
  );
}

Here value={name} ties the displayed text to state, and onChange reads the new text from e.target.value and writes it back. The current value of the field is always available as the name variable—no DOM querying required.

The single source of truth

Because state owns the value, you can derive other UI directly from it. The input never holds data that React doesn’t know about, so live previews, character counts, and validation come for free.

function Bio() {
  const [bio, setBio] = useState("");
  const remaining = 140 - bio.length;

  return (
    <div>
      <textarea
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        maxLength={140}
      />
      <p>{remaining} characters left</p>
    </div>
  );
}

You can also transform input as it arrives—forcing uppercase, stripping spaces, or filtering to digits—simply by changing the value before calling the setter:

onChange={(e) => setPhone(e.target.value.replace(/\D/g, ""))}

Because the value passes through your handler, you control exactly what lands in state. Returning early or sanitizing in onChange lets you reject characters the user can never even type.

Handling many fields with one handler

Writing a separate handler per field gets tedious. A common pattern is to store related fields in a single object and use a generic handler keyed by the input’s name attribute.

import { useState } from "react";

function SignupForm() {
  const [form, setForm] = useState({ email: "", username: "", password: "" });

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

  function handleSubmit(e) {
    e.preventDefault();
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="password" type="password" value={form.password} onChange={handleChange} />
      <button type="submit">Sign up</button>
    </form>
  );
}

Output:

// After typing and clicking "Sign up":
{ email: "[email protected]", username: "ada", password: "hunter2" }

The computed key [name]: value matches each input’s name to the matching field, and the updater function prev => ({ ...prev }) copies the existing object so the other fields aren’t lost. Resetting the whole form is then a one-liner: setForm({ email: "", username: "", password: "" }).

Checkboxes, selects, and other input types

Different elements expose their value differently. The table below summarizes what to bind for each.

ElementBind toRead fromNotes
<input type="text">valuee.target.valueAlso numbers, email, password
<input type="checkbox">checkede.target.checkedBoolean, not a string
<input type="radio">checkede.target.valueCompare to selected value
<select>valuee.target.valueSet on <select>, not <option>
<textarea>valuee.target.valueValue is a prop, not children
function Preferences() {
  const [opts, setOpts] = useState({ newsletter: false, plan: "free" });

  return (
    <form>
      <label>
        <input
          type="checkbox"
          checked={opts.newsletter}
          onChange={(e) =>
            setOpts((p) => ({ ...p, newsletter: e.target.checked }))
          }
        />
        Subscribe
      </label>
      <select
        value={opts.plan}
        onChange={(e) => setOpts((p) => ({ ...p, plan: e.target.value }))}
      >
        <option value="free">Free</option>
        <option value="pro">Pro</option>
      </select>
    </form>
  );
}

The read-only warning

If you set value but forget onChange, React makes the field read-only and warns you in the console. The input shows the state value but ignores typing, because there is no path for changes to flow back.

Output:

Warning: 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`.

To fix it, add an onChange. If you genuinely want a fixed, non-editable field, add readOnly instead. And if you want the DOM to manage the value with only an initial seed—an uncontrolled input—use defaultValue rather than value.

Best Practices

  • Always pair value with onChange; together they form the controlled-component contract.
  • Use one object plus a name-keyed handler when a form has several related fields.
  • Read checkboxes from e.target.checked and everything else from e.target.value.
  • Sanitize or transform input inside onChange so invalid data never enters state.
  • Reset forms by setting state back to the initial object, not by touching the DOM.
  • Add readOnly for intentionally fixed fields, and defaultValue for uncontrolled ones, to silence the read-only warning correctly.
Last updated June 14, 2026
Was this helpful?