Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 19 of 38

Controlled inputs

Connect text fields, textareas, selects, checkboxes and radio buttons to React state with value and onChange, and handle many fields with one change handler.

18 minFormsUpdated 24 October 2026

By the end of this lessonBuild form fields whose values live in state, and update an object of fields with a single handler.

Forms are where most applications meet the user. In plain HTML the browser keeps the text you type inside each input. In React you usually keep it in state instead. An input whose value is driven by state is called a controlled input, and it gives you full power: validate as the user types, format the text, enable or disable a button, reset the form, and reuse the value anywhere.

A controlled text input

Two props make an input controlled: value, which says what to show, and onChange, which says what to do when the user types:

jsx
import { useState } from "react";

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

  return (
    <div>
      <label>
        Your name: <input value={name} onChange={event => setName(event.target.value)} />
      </label>
      <p>Hello, {name || "stranger"}!</p>
    </div>
  );
}

export default App;
result
Your name: 
Hello, Asha!

The cycle on each keystroke is: the user types a letter, React calls onChange with an event, the handler calls setName(event.target.value), React renders again, and the input shows the new name. The input never keeps its own text; it always displays whatever state says. The greeting below it is simply a second place that reads the same state, which is why it updates as you type with no extra code.

What goes wrong without onChange

If you set value but forget onChange, the field becomes read-only. React tells you in the console:

jsx
function App() {
  return <input value="fixed" />;
}

export default App;
result
CONSOLE
error: 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`.

The warning explains that you provided a value prop without an onChange handler. If you want an initial value that the user can still edit, and you do not need to track every keystroke, use defaultValue="fixed" instead (an uncontrolled input, discussed in the Refs lesson).

Other kinds of field

The pattern is the same for every field type, with small differences in the property that holds the value.

Textarea and select

In React, <textarea> takes a value prop (not children as in HTML), and <select> takes a value that matches one of its options (instead of selected on an <option>):

jsx
import { useState } from "react";

function App() {
  const [comment, setComment] = useState("");
  const [language, setLanguage] = useState("en");

  return (
    <form>
      <p>
        <textarea value={comment} onChange={e => setComment(e.target.value)} rows={2} placeholder="Comment" />
      </p>
      <p>
        <select value={language} onChange={e => setLanguage(e.target.value)}>
          <option value="en">English</option>
          <option value="es">Spanish</option>
          <option value="hi">Hindi</option>
        </select>
      </p>
      <p>Comment: "{comment}" in {language}</p>
    </form>
  );
}

export default App;
result
English
Spanish
Hindi
Comment: "Great course" in es

Checkbox and radio buttons

These have a boolean state instead of text. Use checked and event.target.checked. For radios, each button is checked when its value equals the state:

jsx
import { useState } from "react";

function App() {
  const [subscribed, setSubscribed] = useState(false);
  const [plan, setPlan] = useState("free");

  return (
    <form>
      <p>
        <label>
          <input type="checkbox" checked={subscribed} onChange={e => setSubscribed(e.target.checked)} />
          {" "}Send me the newsletter
        </label>
      </p>
      <p>
        {["free", "pro"].map(option => (
          <label key={option} style={{ marginRight: 12 }}>
            <input type="radio" name="plan" value={option} checked={plan === option} onChange={e => setPlan(e.target.value)} />
            {" "}{option}
          </label>
        ))}
      </p>
      <p>Subscribed: {String(subscribed)}; plan: {plan}</p>
    </form>
  );
}

export default App;
result
Send me the newsletter
 free pro
Subscribed: true; plan: pro

Many fields, one state object, one handler

A form with ten fields does not need ten useState calls and ten handlers. Keep the values in a single object, give each input a name, and write one handler that uses that name as the key:

jsx
import { useState } from "react";

function App() {
  const [form, setForm] = useState({ name: "", email: "", agree: false });

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

  return (
    <form>
      <p><input name="name" value={form.name} onChange={handleChange} placeholder="Name" /></p>
      <p><input name="email" type="email" value={form.email} onChange={handleChange} placeholder="Email" /></p>
      <p>
        <label>
          <input name="agree" type="checkbox" checked={form.agree} onChange={handleChange} />
          {" "}I agree
        </label>
      </p>
      <pre>{JSON.stringify(form)}</pre>
    </form>
  );
}

export default App;
result
I agree
{"name":"Asha","email":"a@example.com","agree":true}

Two details do the work. The [name]: ... syntax is a computed property name: the part in square brackets is evaluated and used as the key, so typing in the field named email updates form.email. And { ...form, ... } makes a copy of the object with that single property replaced, so the old state is never mutated (the previous lesson's rule). Using event.target to read name, value, type and checked lets one function serve text fields and checkboxes.

If you call the setter twice quickly or in async code, prefer the updater form setForm(f => ({ ...f, [name]: value })) to avoid stale snapshots. Read the value from event.target before any async step, or copy it into a variable: it is the same snapshot idea.

Showing and using the values

Because the values are state, you can use them anywhere while rendering. Disable a button until the form is valid, show a character counter, or format text:

jsx
import { useState } from "react";

function App() {
  const [text, setText] = useState("");
  const max = 10;

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value.slice(0, max))} />
      <p>{text.length} / {max} characters</p>
      <button disabled={text.length === 0}>Save</button>
      <p>Uppercase preview: {text.toUpperCase()}</p>
    </div>
  );
}

export default App;
result
5 / 10 characters
Save
Uppercase preview: HELLO

The field refuses characters beyond ten (the handler trims the value before storing it), and the button is disabled while the text is empty. All of that is ordinary JavaScript on top of state. A controlled input makes this kind of logic easy because React is the one deciding what the field shows.

Common patterns

  • Reset the form: set the state back to its initial object: setForm(initialForm). The inputs follow.
  • Number fields: event.target.value is always a string, even for type="number". Convert with Number(value) when you use it as a number, and keep the raw string in state if the user may type partial values like 1. or an empty field.
  • Trimming and formatting: apply them in the handler (before setState) or when displaying, depending on whether the stored value should be raw.
  • Many similar fields: map over an array of field definitions to render them.

Common mistakes

  • Setting value without onChange, making the field read-only.
  • Setting value={undefined} or null at first, which makes React treat the input as uncontrolled and later controlled (it warns). Start with an empty string.
  • Using checked with event.target.value (it is always the string "on"); use event.target.checked.
  • Mutating the state object in the handler instead of copying it.
  • Forgetting name on the inputs when using a shared handler.
  • Forgetting that value is a string for number inputs.

Exercise

  1. Add a "Country" select with three options to the multi-field form and show it in the JSON.
  2. Add a "Reset" button that returns every field to its initial value.
  3. Build a password field with a "Show password" checkbox that switches the input between type="password" and type="text".
  4. Enable the Submit button only when the name is not empty, the e-mail contains @, and the checkbox is ticked.

Key points

  • A controlled input gets its value from state (value or checked) and reports typing through onChange, so state is the single source of truth.
  • Text, textarea and select use value and event.target.value; checkboxes use checked and event.target.checked.
  • Give each field a name and use one handler with [event.target.name]: event.target.value to update an object of values.
  • An input with a value but no onChange is read-only and React warns; use defaultValue for an uncontrolled starting value.

Check yourself

1. What makes an input 'controlled'?
2. Which attribute holds a checkbox's state?
3. What happens if you give an input value={text} but no onChange?
4. In a shared handler, what does [event.target.name]: event.target.value do?