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.
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 itWhy 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
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:
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 propControlled 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:
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:
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
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:
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 |
Instant validation | Easy | Harder |
Pre-filling values | Easy ( | Use |
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 |
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.