Controlled Inputs

Controlled and Uncontrolled Inputs

An input is controlled when you pass it a value (or checked) prop and an onChange handler: React 7,897 renders the value from state, the handler writes the next value back, and the DOM node is only ever a mirror. Pass value without onChange and you get a field the user cannot type in, plus a console warning — a fixed value is a read-only field, not a starting value. Pass defaultValue instead and the input is uncontrolled: the DOM owns the text and you read it on submit. JSX also smooths over two DOM inconsistencies: <textarea> takes a value prop rather than children, and <select> takes value rather than a selected attribute on one <option>.

A controlled form mirroring its own stateHTML
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
function Rsvp() {
  const [form, setForm] = useState({ going: true, flavor: 'lime', note: 'Bringing cake' });
  function handleChange(e) {
    const t = e.target;                       // one handler for every field
    setForm({ ...form, [t.name]: t.type === 'checkbox' ? t.checked : t.value });
  }
  return (
    <form style={{ font: '15px system-ui' }}>
      <label><input name="going" type="checkbox" checked={form.going}
                    onChange={handleChange} /> Going</label>{' '}
      <select name="flavor" value={form.flavor} onChange={handleChange}>
        <option value="lime">Lime</option><option value="mango">Mango</option>
      </select>
      <textarea name="note" rows="2" value={form.note} onChange={handleChange} />
      <pre>{JSON.stringify(form)}</pre>
    </form>
  );
}
createRoot(document.getElementById('root')).render(<Rsvp />);
</script>
Browser output of Listing 2.68
Browser output of 68

The computed property name [t.name] is what keeps this to one handler: name each field after its state key and the handler never grows. Reach for controlled inputs when something must react to every keystroke — formatting a phone number, filtering a list, disabling a second field based on a first. The cost is a re-render of the whole form per keystroke, visible in a fifty-field form; that is why uncontrolled inputs and the form actions of Form Actions scale better. File inputs are always uncontrolled, since value on <input type="file"> is read-only. And never let an input switch from uncontrolled to controlled mid-life — state initialized to undefined and filled from a fetch does exactly that, warns, and can lose the user's typing.