ReactControlled Components

Controlled Components

A controlled component is a form input whose value is driven by React state. Instead of the DOM tracking what the user has typed, React is the single source of truth — every keystroke updates state, and React re-renders the input with the new value. This gives you complete, fine-grained control over your form data.

The Core Idea: Two-Way Binding

Controlled components implement a two-way flow: React state flows down into the input via the value prop, and user input flows up via the onChange event. State is always synchronized with what the user sees.

JSX
import { useState } from 'react'

function ControlledInput() {
  const [text, setText] = useState('')

  return (
    <div>
      <input
        value={text}            // React state flows INTO the input
        onChange={e => setText(e.target.value)} // DOM event updates state
      />
      <p>You typed: {text}</p>  // always in sync
    </div>
  )
}

// Without the controlled pattern:
// <input />  — DOM owns the value; React doesn't know what's in it
Why Use Controlled Components?
  • Instant validation — validate as the user types, not just on submit

  • Programmatic updates — set the input value from code (e.g., pre-fill from an API)

  • Conditional form fields — show/hide fields based on other field values

  • Formatting — format phone numbers, credit card numbers as they're typed

  • Derived state — compute character count, password strength, etc.

  • Full-form state — know all field values at any time, not just on submit

Controlled Text Input

JSX
function UsernameInput() {
  const [username, setUsername] = useState('')

  // Transform input: lowercase, no spaces
  function handleChange(e) {
    const clean = e.target.value.toLowerCase().replace(/s/g, '')
    setUsername(clean)
  }

  const isValid = username.length >= 3 && /^[a-z0-9_]+$/.test(username)

  return (
    <div>
      <label htmlFor="username">Username</label>
      <input
        id="username"
        type="text"
        value={username}
        onChange={handleChange}
        maxLength={20}
      />
      <p>{username.length}/20 characters</p>
      {username.length > 0 && !isValid && (
        <p className="error">
          3-20 characters, letters, numbers, and underscores only
        </p>
      )}
    </div>
  )
}
Controlled Textarea

textarea in React works like input — use value and onChange. Note that React's textarea is self-closing or has no content between tags; the value goes in the value prop, not between the tags:

JSX
function BioInput() {
  const [bio, setBio] = useState('')
  const MAX = 280

  return (
    <div>
      <label htmlFor="bio">Bio</label>
      <textarea
        id="bio"
        value={bio}
        onChange={e => setBio(e.target.value)}
        maxLength={MAX}
        rows={4}
        placeholder="Tell us about yourself"
      />
      <p style={{ color: bio.length > MAX * 0.9 ? 'red' : 'gray' }}>
        {MAX - bio.length} characters remaining
      </p>
    </div>
  )
}

// In HTML: <textarea>initial value</textarea>
// In React: <textarea value="initial value" onChange={...} />  ← value prop
Controlled Select

For select elements, pass the currently selected value as the value prop and handle onChange. React handles the selected attribute on the option elements automatically:

JSX
function CountrySelect() {
  const [country, setCountry] = useState('us')

  return (
    <div>
      <label htmlFor="country">Country</label>
      <select
        id="country"
        value={country}
        onChange={e => setCountry(e.target.value)}
      >
        <option value="">-- Select a country --</option>
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="uk">United Kingdom</option>
        <option value="de">Germany</option>
      </select>
      <p>Selected: {country}</p>
    </div>
  )
}

// Multi-select: pass an array as value, handle as array
function MultiSelect() {
  const [selected, setSelected] = useState([])

  function handleChange(e) {
    const options = Array.from(e.target.selectedOptions, opt => opt.value)
    setSelected(options)
  }

  return (
    <select multiple value={selected} onChange={handleChange}>
      <option value="react">React</option>
      <option value="vue">Vue</option>
      <option value="angular">Angular</option>
    </select>
  )
}
Controlled Checkbox

Checkboxes use checked (not value) for the controlled prop, and e.target.checked in the onChange handler:

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

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={agreed}
          onChange={e => setAgreed(e.target.checked)}
        />
        I agree to the Terms of Service
      </label>
      <button disabled={!agreed} type="submit">
        Continue
      </button>
    </div>
  )
}

// Multiple checkboxes — store as an array of selected values
function InterestPicker() {
  const [interests, setInterests] = useState([])
  const options = ['React', 'TypeScript', 'Node.js', 'GraphQL']

  function handleChange(e) {
    const value = e.target.value
    setInterests(prev =>
      e.target.checked
        ? [...prev, value]
        : prev.filter(v => v !== value)
    )
  }

  return (
    <fieldset>
      <legend>Your interests</legend>
      {options.map(opt => (
        <label key={opt}>
          <input
            type="checkbox"
            value={opt}
            checked={interests.includes(opt)}
            onChange={handleChange}
          />
          {opt}
        </label>
      ))}
    </fieldset>
  )
}
Controlled Radio Buttons

JSX
function PaymentMethod() {
  const [method, setMethod] = useState('card')

  return (
    <fieldset>
      <legend>Payment method</legend>

      {['card', 'paypal', 'bank'].map(option => (
        <label key={option}>
          <input
            type="radio"
            value={option}
            checked={method === option}
            onChange={e => setMethod(e.target.value)}
          />
          {option.charAt(0).toUpperCase() + option.slice(1)}
        </label>
      ))}

      <p>Selected: {method}</p>
    </fieldset>
  )
}
Managing Multiple Fields with One Handler

For forms with many fields, use a single state object and a single onChange handler that uses e.target.name to identify which field changed:

JSX
function RegistrationForm() {
  const [form, setForm] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
  })

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

  function handleSubmit(e) {
    e.preventDefault()
    console.log(form) // { firstName, lastName, email, password }
    registerUser(form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="firstName"
        value={form.firstName}
        onChange={handleChange}
        placeholder="First name"
      />
      <input
        name="lastName"
        value={form.lastName}
        onChange={handleChange}
        placeholder="Last 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">Register</button>
    </form>
  )
}
Controlled vs Uncontrolled: Quick Comparison

Aspect

Controlled

Uncontrolled

Source of truth

React state

DOM

Reading values

Always available in state

Read via ref on submit

Instant validation

Easy

Harder

Pre-filling values

Easy (useState initial value)

Use defaultValue prop

Formatting as user types

Easy

Very difficult

Re-renders per keystroke

Yes

No

Code volume

More verbose

Less verbose

Best for

Most React forms

Simple forms, file inputs

Warning
A controlled input **must** have both a `value` prop and an `onChange` handler. If you supply `value` without `onChange`, the input becomes read-only and React logs a warning: "You provided a `value` prop to a form field without an `onChange` handler." Use `defaultValue` if you want an uncontrolled input with an initial value.
Performance: Re-renders on Every Keystroke

Controlled components re-render on every keystroke. For most forms this is imperceptible — React's reconciler is fast and updating a single input is a trivial diff. Only optimize if you observe real jank in profiling.

If you have very large forms (dozens of fields) or expensive child components, consider React Hook Form, which uses uncontrolled inputs by default and only triggers re-renders when explicitly needed.

Note
React Hook Form's controlled mode (`useController` or the `Controller` component) gives you the best of both worlds: controlled API (instant validation, values always available) with minimized re-renders. It's the industry standard for complex forms.
Tip
Start with simple controlled components — `useState` + `onChange`. Only reach for React Hook Form when you have validation requirements that become complex (Zod schemas, cross-field dependencies, async validation) or when a large form is noticeably slow.