| |

React 25 ⚛️ Controlled Form Inputs

A form input that holds its own state is unpredictable. The DOM remembers what the user typed, but your React component does not, unless you ask it. When you need to validate, transform, or react to input as it happens, you must take control of the value. This is the controlled component pattern: the input’s value is driven by React state, and every keystroke flows through an event handler that updates that state. The input becomes a reflection of your component’s data, not an independent store of truth.

This chapter covers controlled form inputs in React. You will learn how the value and onChange pair creates a controlled input, how this differs from uncontrolled inputs that rely on the DOM, and how to handle different input types—text, textarea, select, checkbox, radio, and file. You will see how to build reusable form components with a single change handler, how to validate and transform input as it is typed, and how to avoid the common pitfalls that produce read-only fields or laggy typing.

By the end, you will understand controlled inputs as the foundation of predictable forms in React, and you will know when an uncontrolled input is the better choice.

Key point: In a controlled input, React state is the single source of truth. The input’s value prop is set from state, and the onChange handler updates that state. If you set value without onChange, the input becomes read-only because React re-renders it to the same value on every keystroke.


Why controlled inputs exist

The single source of truth problem. In plain HTML, an input holds its own value. If your JavaScript needs to know what the user typed, it must read the DOM. In React, the component’s state is the source of truth for rendering. If state and the DOM can disagree, bugs follow: a form reset leaves the input unchanged, a validation message contradicts what the user sees, an undo does not restore the previous value. Controlled inputs eliminate this by making state the only source of the value.

The reactivity problem. Controlled inputs let you respond to every keystroke. You can validate as the user types, format a phone number, disable a submit button until the form is valid, or filter a list based on the current query. Uncontrolled inputs do not offer this without reading the DOM imperatively.

The consistency problem. A controlled input always displays what state says it should display. When the component re-renders—because a parent passed new props, because a sibling changed, because a timer fired—the input shows the current state. There is no way for the DOM to drift out of sync.

The testing problem. Controlled components are easier to test because the value is a prop, and the change handler is a function. You can render the component with a given value and assert that the DOM reflects it. You can call the change handler with a synthetic event and assert that state updates.

The form library foundation. Every form library in React—Formik, React Hook Form, TanStack Form—builds on the controlled input pattern, sometimes with uncontrolled performance optimizations underneath. Understanding controlled inputs is understanding the foundation that these libraries abstract.


a. Text inputs and the value/onChange pair

A controlled text input sets its value from state and updates that state in onChange.

import { useState } from 'react';

function NameForm() {
  const [name, setName] = useState('');

  return (
    <form>
      <label>
        Name:
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <p>Hello, {name || 'stranger'}!</p>
    </form>
  );
}

The value prop binds the input to name. The onChange handler reads e.target.value—the current text in the input—and calls setName with it. React re-renders, the input receives the new value, and the displayed text matches state.

The e.target.value access is important. The event object is React’s synthetic event, which wraps the native event. e.target is the input element, and value is its current value. This is the standard pattern for text inputs.

If you set value but forget onChange, the input becomes read-only. React re-renders the input with the same value after every keystroke, effectively undoing the user’s input. This is a common mistake and produces an input that looks broken.

If you omit value entirely, the input is uncontrolled. It works, but React cannot read or set its value without a ref. This is fine for simple cases but loses the benefits of controlled inputs.


b. Textarea, select, and other input types

React handles <textarea> differently from HTML. In HTML, <textarea> uses its children as the value. In React, it uses the value prop, just like <input>.

function BioForm() {
  const [bio, setBio] = useState('');

  return (
    <textarea
      value={bio}
      onChange={(e) => setBio(e.target.value)}
      rows={4}
    />
  );
}

A <select> uses value and onChange on the select element itself.

function FruitPicker() {
  const [fruit, setFruit] = useState('apple');

  return (
    <select value={fruit} onChange={(e) => setFruit(e.target.value)}>
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
      <option value="cherry">Cherry</option>
    </select>
  );
}

The value on the <select> matches one of the <option> values. React selects the matching option. In HTML you would use selected on the option; in React, you set value on the select.

Checkboxes use checked instead of value.

function TermsCheckbox() {
  const [agreed, setAgreed] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={agreed}
        onChange={(e) => setAgreed(e.target.checked)}
      />
      I agree to the terms
    </label>
  );
}

The handler reads e.target.checked, which is a boolean, not e.target.value.

Radio buttons use checked and a shared state value.

function ColorPicker() {
  const [color, setColor] = useState('red');

  return (
    <div>
      {['red', 'green', 'blue'].map((c) => (
        <label key={c}>
          <input
            type="radio"
            value={c}
            checked={color === c}
            onChange={(e) => setColor(e.target.value)}
          />
          {c}
        </label>
      ))}
    </div>
  );
}

Each radio has the same name if you want them to be mutually exclusive natively, but React does not require it. The checked prop compares the current state to the radio’s value.

File inputs are special. Their value cannot be set programmatically for security reasons, so they are always uncontrolled. You read the file via a ref or the event’s files property.

function FileUpload() {
  const [fileName, setFileName] = useState('');

  const handleChange = (e) => {
    setFileName(e.target.files[0]?.name ?? '');
  };

  return (
    <div>
      <input type="file" onChange={handleChange} />
      {fileName && <p>Selected: {fileName}</p>}
    </div>
  );
}

You can track the file name in state, but the file input itself remains uncontrolled.


c. Reusable handlers and form state

When a form has many fields, one state variable per field becomes verbose. A common pattern is a single state object with a generic change handler.

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

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(form);
  };

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

The handler reads e.target.name to know which field changed, and e.target.value for the new value. It updates the state object using the computed property name. This pattern scales to any number of text-like inputs.

The functional update form setForm((prev) => ({ ...prev, [name]: value })) is important. It ensures the update is based on the latest state, which matters when multiple updates are batched. It also avoids stale closures when the handler is memoized.

For checkboxes in a multi-field form, the handler must distinguish between checked and value:

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

This handles both text and checkbox inputs with one handler.

Validation can be computed from the form state and rendered conditionally:

const errors = {
  email: !form.email.includes('@') ? 'Invalid email' : '',
  password: form.password.length < 8 ? 'Too short' : ''
};

The errors object is derived from state, not stored separately. This keeps the source of truth single and avoids sync bugs.


Complete Example Session

// ============================================
// PART 1: BASIC CONTROLLED TEXT INPUT
// ============================================
import { useState } from 'react';

function NameForm() {
  const [name, setName] = useState('');
  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}
// ============================================
// PART 2: CONTROLLED TEXTAREA
// ============================================
function BioForm() {
  const [bio, setBio] = useState('');
  return (
    <textarea
      value={bio}
      onChange={(e) => setBio(e.target.value)}
      rows={4}
    />
  );
}
// ============================================
// PART 3: CONTROLLED SELECT
// ============================================
function FruitPicker() {
  const [fruit, setFruit] = useState('apple');
  return (
    <select value={fruit} onChange={(e) => setFruit(e.target.value)}>
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
    </select>
  );
}
// ============================================
// PART 4: CONTROLLED CHECKBOX
// ============================================
function TermsCheckbox() {
  const [agreed, setAgreed] = useState(false);
  return (
    <label>
      <input
        type="checkbox"
        checked={agreed}
        onChange={(e) => setAgreed(e.target.checked)}
      />
      I agree
    </label>
  );
}
// ============================================
// PART 5: CONTROLLED RADIO GROUP
// ============================================
function ColorPicker() {
  const [color, setColor] = useState('red');
  return (
    <div>
      {['red', 'green', 'blue'].map((c) => (
        <label key={c}>
          <input
            type="radio"
            value={c}
            checked={color === c}
            onChange={(e) => setColor(e.target.value)}
          />
          {c}
        </label>
      ))}
    </div>
  );
}
// ============================================
// PART 6: SINGLE STATE OBJECT
// ============================================
function SignupForm() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    password: ''
  });

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

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="password" value={form.password} onChange={handleChange} />
    </form>
  );
}
// ============================================
// PART 7: HANDLER FOR CHECKBOX AND TEXT
// ============================================
function MixedForm() {
  const [form, setForm] = useState({ email: '', subscribe: false });

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

  return (
    <form>
      <input name="email" value={form.email} onChange={handleChange} />
      <label>
        <input
          type="checkbox"
          name="subscribe"
          checked={form.subscribe}
          onChange={handleChange}
        />
        Subscribe
      </label>
    </form>
  );
}
// ============================================
// PART 8: VALIDATION FROM STATE
// ============================================
function ValidatedForm() {
  const [email, setEmail] = useState('');
  const isValid = email.includes('@') && email.includes('.');

  return (
    <div>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {!isValid && email && <p>Invalid email</p>}
      <button disabled={!isValid}>Submit</button>
    </div>
  );
}
// ============================================
// PART 9: TRANSFORMING INPUT
// ============================================
function PhoneInput() {
  const [phone, setPhone] = useState('');

  const handleChange = (e) => {
    const digits = e.target.value.replace(/\D/g, '');
    const formatted = digits.replace(
      /(\d{3})(\d{3})(\d{4})/,
      '($1) $2-$3'
    );
    setPhone(formatted);
  };

  return <input value={phone} onChange={handleChange} />;
}
// ============================================
// PART 10: FILE INPUT (UNCONTROLLED)
// ============================================
function FileUpload() {
  const [fileName, setFileName] = useState('');

  const handleChange = (e) => {
    setFileName(e.target.files[0]?.name ?? '');
  };

  return (
    <div>
      <input type="file" onChange={handleChange} />
      {fileName && <p>Selected: {fileName}</p>}
    </div>
  );
}

The ten parts covered the full range of controlled inputs: text, textarea, select, checkbox, radio, a single state object with a generic handler, a handler that distinguishes checkbox from text, validation derived from state, input transformation, and the file input as an exception.


Quick Reference

Controlled Input Pattern

ElementValue PropChange Handler Reads
<input type="text">valuee.target.value
<textarea>valuee.target.value
<select>valuee.target.value
<input type="checkbox">checkede.target.checked
<input type="radio">checkede.target.value
<input type="file">N/A (uncontrolled)e.target.files

Controlled vs Uncontrolled

AspectControlledUncontrolled
Source of truthReact stateDOM
Value propRequiredOmitted
Change handlerRequiredOptional
Read valueFrom stateFrom ref
Validation as typingYesNo
Form resetSet stateUse ref or key
File inputsNot possibleRequired

Form State Patterns

PatternWhen to Use
One useState per fieldSmall forms (2–3 fields)
Single state objectMedium forms (4+ fields)
Generic handlerAny form with named inputs
useReducerComplex form logic
Form libraryLarge or complex forms

Common Input Types

TypeControlled PropHandler
textvaluesetValue(e.target.value)
emailvaluesetValue(e.target.value)
passwordvaluesetValue(e.target.value)
numbervaluesetValue(e.target.value)
checkboxcheckedsetChecked(e.target.checked)
radiocheckedsetValue(e.target.value)
file—e.target.files

Best Practices

✅ Do This:

// Always pair value with onChange
<input value={name} onChange={(e) => setName(e.target.value)} />  // ✅

// Use e.target.checked for checkboxes
<input type="checkbox" checked={ok}
       onChange={(e) => setOk(e.target.checked)} />                // ✅

// Use a single state object for many fields
const [form, setForm] = useState({ name: '', email: '' });         // ✅

// Use functional updates in the handler
setForm((prev) => ({ ...prev, [name]: value }));                   // ✅

// Derive validation from state
const isValid = email.includes('@');                               // ✅

// Use name attributes for generic handlers
<input name="email" value={form.email} onChange={handleChange} />  // ✅

❌ Don’t Do This:

// Set value without onChange (read-only)
<input value={name} />                                             // ❌

// Use value with checkbox
<input type="checkbox" value={ok} />                               // ❌

// Read e.target.value for checkbox
setOk(e.target.value);                                             // ❌ (use checked)

// Store validation in separate state
const [isValid, setIsValid] = useState(false);                     // ⚠️ (derive instead)

// Try to control file input
<input type="file" value={file} />                                 // ❌ (not allowed)

// Forget the functional update when memoizing
const handleChange = useCallback((e) => {
  setForm({ ...form, [e.target.name]: e.target.value });
}, [form]);                                                        // ⚠️ (stale risk)

Common Pitfalls

PitfallWhy It HappensFix
Input is read-onlyvalue set without onChangeAdd onChange handler
Typing laggyExpensive computation in onChangeDebounce or move to useMemo
Checkbox toggles wrongReading value instead of checkedUse e.target.checked
Select doesn’t updatevalue not matching option valueMatch exactly
State stale in handlerClosure over old stateUse functional update
File input errorvalue prop on file inputRemove value, read files
Form does not resetState not clearedReset all state fields

Real-World Examples

1. Login Form

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

2. Search Input

<input
  value={query}
  onChange={(e) => setQuery(e.target.value)}
  placeholder="Search..."
/>

3. Multi-Field Signup

const [form, setForm] = useState({ name: '', email: '', password: '' });

4. Checkbox Agreement

<input
  type="checkbox"
  checked={agreed}
  onChange={(e) => setAgreed(e.target.checked)}
/>

5. Radio Group

{options.map((opt) => (
  <label key={opt}>
    <input
      type="radio"
      value={opt}
      checked={selected === opt}
      onChange={(e) => setSelected(e.target.value)}
    />
    {opt}
  </label>
))}

6. Select Dropdown

<select value={country} onChange={(e) => setCountry(e.target.value)}>
  {countries.map((c) => <option key={c} value={c}>{c}</option>)}
</select>

7. Textarea with Character Count

<textarea
  value={bio}
  onChange={(e) => setBio(e.target.value)}
  maxLength={200}
/>
<p>{bio.length}/200</p>

8. Formatted Phone Number

const digits = e.target.value.replace(/\D/g, '');

9. Disabled Submit Button

<button disabled={!isValid}>Submit</button>

10. Reset Form

const reset = () => {
  setForm({ name: '', email: '', password: '' });
};

Visual

Controlled Input Data Flow

┌─────────────────────────────────────────────────────────────┐
│  CONTROLLED INPUT FLOW                                      │
│                                                             │
│  User types "H"                                             │
│    │                                                        │
│    ▼                                                        │
│  onChange fires                                             │
│    │                                                        │
│    ▼                                                        │
│  setName(e.target.value)                                    │
│    │                                                        │
│    ▼                                                        │
│  State updates: name = "H"                                  │
│    │                                                        │
│    ▼                                                        │
│  React re-renders                                           │
│    │                                                        │
│    ▼                                                        │
│  <input value="H" />                                        │
│    │                                                        │
│    ▼                                                        │
│  Input displays "H"                                         │
│                                                             │
│  State is the single source of truth.                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Read-Only Bug

┌─────────────────────────────────────────────────────────────┐
│  VALUE WITHOUT ONCHANGE                                     │
│                                                             │
│  <input value={name} />                                     │
│                                                             │
│  User types "H"                                             │
│    │                                                        │
│    ▼                                                        │
│  No onChange → no state update                              │
│    │                                                        │
│    ▼                                                        │
│  React re-renders with value={name} (unchanged)             │
│    │                                                        │
│    ▼                                                        │
│  Input reverts to original value                            │
│                                                             │
│  Result: input appears read-only.                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Controlled vs Uncontrolled

┌─────────────────────────────────────────────────────────────┐
│  CONTROLLED                                                 │
│                                                             │
│  React state ──▶ value ──▶ input                            │
│       ▲                       │                             │
│       └──── onChange ◀────────┘                             │
│                                                             │
│  State drives display. Every change flows through React.    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  UNCONTROLLED                                               │
│                                                             │
│  input ──▶ DOM value (React does not track)                 │
│                                                             │
│  Read via ref when needed. React does not re-render         │
│  on change.                                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Form State Object

┌─────────────────────────────────────────────────────────────┐
│  SINGLE STATE OBJECT                                        │
│                                                             │
│  const [form, setForm] = useState({                         │
│    name: '',                                                │
│    email: '',                                               │
│    password: ''                                             │
│  });                                                        │
│                                                             │
│  handleChange(e):                                           │
│    const { name, value } = e.target;                        │
│    setForm(prev => ({ ...prev, [name]: value }));           │
│                                                             │
│  Each input:                                                │
│    <input name="email"                                      │
│           value={form.email}                                │
│           onChange={handleChange} />                        │
│                                                             │
│  One handler updates any field by name.                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Patternvalue={state} + onChange={handler}
Text inputvalue + e.target.value
TextareaSame as text input in React
Selectvalue on select, e.target.value
Checkboxchecked + e.target.checked
Radiochecked + e.target.value
File inputUncontrolled; read e.target.files
Single state objectuseState({...}) for many fields
Generic handlerRead e.target.name to update field
Functional updatesetForm(prev => ({...prev, [name]: value}))
ValidationDerived from state, not stored

Key takeaways:

  • Controlled inputs make React state the single source of truth. The value prop is set from state, and the onChange handler updates that state. The input always reflects what state says.
  • value without onChange makes the input read-only. React re-renders the input with the same value on every keystroke, undoing the user’s input. Both props are required for a controlled input.
  • Different input types use different props and event fields. Text inputs use value and e.target.value. Checkboxes and radios use checked and e.target.checked or e.target.value. File inputs cannot be controlled.
  • A single state object scales to many fields. Store form data in one object, use e.target.name to identify the field, and update with a functional spread. This avoids one useState per field.
  • The functional update prevents stale state. setForm(prev => ({...prev, [name]: value})) uses the latest state, which matters when updates are batched or the handler is memoized.
  • Validation should be derived, not stored. Compute validity from the form state on each render. Storing it in separate state creates sync problems.
  • Input transformation happens in the handler. To format a phone number or uppercase a field, transform e.target.value before calling setState. The transformed value flows back to the input through value.
  • File inputs are always uncontrolled. Their value cannot be set programmatically. Track metadata like the file name in state if needed, but read the file itself from the event or a ref.

Remember: Controlled inputs are the foundation of predictable forms in React. They make the component the source of truth, enable validation and transformation as the user types, and keep the displayed value in sync with the data. The pattern is simple: value from state, onChange to update state. The complexity comes from the variety of input types and the need for a scalable handler. Use a single state object with a generic handler for forms with many fields, derive validation from state, and remember that file inputs are the exception. When the form is small and you do not need to react to input, an uncontrolled input with a ref is a reasonable choice. For everything else, control the input, and the form will behave predictably.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!