React: Forms and Controlled Components
Last updated: 2026-08-26
Forms serve as the bridge between web applications and users. In React, forms are no longer just about "filling them out and submitting them"—instead, "React knows every character you type." This pattern is called a controlled component.
1. What You'll Learn
- Controlled Components vs. Uncontrolled Components
- input、textarea、select、checkbox、radio 's controlled approach
- Form Submission Process
- Basic Form Validation
- Custom Form Hooks
2. The Evolution of a Registration Form
(1) Pain Point: Difficulty Tracking Data from Uncontrolled Forms
Alice created a user registration form that retrieves data using native methods:
<!-- HTML Form:Read data all at once upon submission -->
<form id="register-form">
<input name="username" id="username" />
<input type="email" name="email" id="email" />
<input type="password" name="password" id="password" />
<select name="role" id="role">
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
<script>
document.getElementById('register-form').addEventListener('submit', function(e) {
e.preventDefault()
// I'll go when I submit it. DOM Data from Zhongna
const username = document.getElementById('username').value
const email = document.getElementById('email').value
const password = document.getElementById('password').value
const role = document.getElementById('role').value
// ... Manually verify each field
})
</script>
Question:
- Lack of transparency: What is being entered? The median value? React doesn't know.
- Difficulty with real-time validation: You have to listen for input events and manually implement the validation logic.
- Trouble with form data binding: Features like "automatically filling in the area code after selecting a country" have to be coded manually
(2) Solutions for React Controlled Components
function RegisterForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
role: 'developer'
})
// Each input change React Everyone knows
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Registration Data:', form) // Retrieve the complete form data
}
return (
<form onSubmit={handleSubmit}>
<input name="username" value={form.username} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" value={form.password} onChange={handleChange} />
<select name="role" value={form.role} onChange={handleChange}>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
)
}
Benefits: React tracks each input field in real time, providing real-time validation and real-time data synchronization, and retrieves the complete data immediately upon submission.
3. Controlled Components vs. Uncontrolled Components
graph TB
subgraph "Controlled Components(Recommendations)"
A1[User Input] --> B1[onChange Event]
B1 --> C1[Update State]
C1 --> D1[React Render Again]
D1 --> E1[value = State]
end
subgraph "Uncontrolled Components"
A2[User Input] --> B2[Update Directly DOM]
B2 --> C2[Use when submitting ref Reading]
end
style C1 fill:#52c41a,color:#fff
style B2 fill:#faad14,color:#000
| Dimension | Controlled Components | Uncontrolled Components |
|---|---|---|
| Data Source | React State (Single Source of Truth) | The DOM Itself |
| Access Method | Read directly from State | Requires a ref or document.getElementById |
| Real-time validation | ✅ Natively supported | ❌ Requires additional monitoring |
| Instant Synchronization | ✅ Real-time data synchronization | ❌ Manual synchronization required |
| Use Cases | Most forms | File uploads, simple one-time forms |
Rule: Use controlled components in 90% of cases. Only the file
<input type="file">must use an uncontrolled component.
4. Controlled Syntax for Various Form Elements
| Form Element | Bound Property | onChange Value | Example |
|---|---|---|---|
<input type="text"> |
value |
e.target.value |
<input value={name} onChange={e => setName(e.target.value)} /> |
<input type="number"> |
value |
Number(e.target.value) |
Must be converted to numeric values manually |
<input type="checkbox"> |
checked |
e.target.checked |
Boolean, not value |
<input type="radio"> |
checked |
e.target.value |
Grouped by name; "checked" controls whether they are selected |
<textarea> |
value |
e.target.value |
React uses value, not child text |
<select> |
value |
e.target.value |
Multiple selections multiple + array |
<input type="file"> |
❌ Uncontrolled | e.target.files[0] |
Can only be used as uncontrolled |
(1) Text Input
function TextInputs() {
const [name, setName] = React.useState('')
const [bio, setBio] = React.useState('')
const [age, setAge] = React.useState(18)
return (
<div>
{/* Single-line text */}
<label>Name:</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Please enter your name"
/>
{/* Numbers */}
<label>Age:</label>
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))} // Convert to a number!
min={0} max={150}
/>
{/* Multi-line text */}
<label>Personal Profile:</label>
<textarea
value={bio}
onChange={e => setBio(e.target.value)}
rows={4}
placeholder="Introduce Yourself..."
/>
</div>
)
}
Note: In React, use the
<textarea>property instead of the child text content. Thee.target.valuevalue for<input type="number">is a string; be sure to convert it to a number.
(2) Checkbox
function SelectInputs() {
const [city, setCity] = React.useState('')
const [hobbies, setHobbies] = React.useState([])
return (
<div>
{/* Single-selection drop-down menu */}
<label>City:</label>
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">-- Please select --</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
<option value="shenzhen">Shenzhen</option>
</select>
{/* Multiple-choice drop-down list */}
<label>Hobbies:</label>
<select
multiple
value={hobbies}
onChange={e => {
const selected = Array.from(e.target.options)
.filter(o => o.selected)
.map(o => o.value)
setHobbies(selected)
}}
>
<option value="coding">Programming</option>
<option value="reading">Read</option>
<option value="gaming">Games</option>
</select>
</div>
)
}
(3) Checkboxes and Radio Buttons
function CheckboxAndRadio() {
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [interests, setInterests] = React.useState([])
// Multiple Choice:Toggle Inclusion/Exclusion
function toggleInterest(value) {
setInterests(prev =>
prev.includes(value)
? prev.filter(i => i !== value)
: [...prev, value]
)
}
return (
<div>
{/* Single checkbox */}
<label>
<input
type="checkbox"
checked={agree}
onChange={e => setAgree(e.target.checked)} // Please note that checked No value
/>
Agree to the User Agreement
</label>
{agree && <p style={{ color: '#52c41a' }}>✅ Agreed</p>}
{/* Radio Button Group:The same one name */}
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '12px' }}>
<input
type="radio"
name="gender"
value={g}
checked={gender === g}
onChange={e => setGender(e.target.value)}
/>
{g}
</label>
))}
{/* Multiple-Selection Checkbox Group */}
<p>Areas of Interest:</p>
{['Front End', 'Backend', 'AI', 'Design'].map(item => (
<label key={item} style={{ marginRight: '12px' }}>
<input
type="checkbox"
checked={interests.includes(item)}
onChange={() => toggleInterest(item)}
/>
{item}
</label>
))}
</div>
)
}
▶ Example 1: Quick Reference Table for Form Element Types
Output:
Form with input fields and submit handling. TypeScript-typed React component with interface props
// ============================================
// Example:Controlled Syntax for All Common Form Elements
// ============================================
function FormElementReference() {
const [text, setText] = React.useState('')
const [number, setNumber] = React.useState(0)
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [bio, setBio] = React.useState('')
const [city, setCity] = React.useState('')
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [file, setFile] = React.useState(null)
return (
<div>
{/* Text Input */}
<input type="text" value={text} onChange={e => setText(e.target.value)} />
{/* Numeric Input */}
<input type="number" value={number} onChange={e => setNumber(Number(e.target.value))} />
{/* Enter your email address */}
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
{/* Password Entry */}
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
{/* Multi-line text */}
<textarea value={bio} onChange={e => setBio(e.target.value)} />
{/* Drop-down menu */}
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">Please select</option>
<option value="beijing">Beijing</option>
</select>
{/* Single checkbox */}
<input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
{/* Radio button */}
<input type="radio" name="gender" value="male" checked={gender === 'male'}
onChange={e => setGender(e.target.value)} />
{/* File Upload(Uncontrolled) */}
<input type="file" onChange={e => setFile(e.target.files[0])} />
{/* Note:File input must be in uncontrolled mode. */}
</div>
)
}
Output:
Reference: text (value/onChange), number (Number() cast), email, password, textarea (value), select (value), checkbox (checked), radio (checked), file (uncontrolled, files[0])
5. Form Validation
| Verification Timing | Triggering Event | Advantages | Disadvantages | Recommended Scenarios |
|---|---|---|---|---|
| During input | onChange | Real-time feedback; users can correct errors immediately | Triggered frequently; an error is reported before the user finishes typing | Password strength, username validation |
| When out of focus | onBlur | Minimize distractions; validate only after completion | Users see errors only after leaving the field | Most form fields |
| Upon submission | onSubmit | Final review and unified validation | User discovers an error only after filling out the form | Final fallback validation |
(1) Real-time Verification
function ValidatedForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({}) // Record which fields have been accessed
function validate(fieldName, value) {
const newErrors = { ...errors }
if (fieldName === 'email') {
if (!value) newErrors.email = 'The email address cannot be left blank.'
else if (!value.includes('@')) newErrors.email = 'The email address format is incorrect.'
else delete newErrors.email
}
if (fieldName === 'password') {
if (!value) newErrors.password = 'The password cannot be empty.'
else if (value.length < 6) newErrors.password = 'Password must be at least 6 chars'
else if (!/[A-Z]/.test(value)) newErrors.password = 'Passwords must contain uppercase letters'
else delete newErrors.password
}
setErrors(newErrors)
}
function handleBlur(e) {
const { name, value } = e.target
setTouched({ ...touched, [name]: true })
validate(name, value)
}
function handleSubmit(e) {
e.preventDefault()
// Validate all fields before submitting
const allErrors = {}
if (!email || !email.includes('@')) allErrors.email = 'Please enter a valid email address'
if (!password || password.length < 6) allErrors.password = 'Password must be at least 6 chars'
if (Object.keys(allErrors).length > 0) {
setErrors(allErrors)
setTouched({ email: true, password: true })
return
}
alert('Submission Successful!')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
<div style={{ marginBottom: '16px' }}>
<label>Email:</label>
<input
name="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); validate('email', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.email && errors.email)}
/>
{touched.email && errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label>Password:</label>
<input
name="password"
type="password"
value={password}
onChange={e => { setPassword(e.target.value); validate('password', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.password && errors.password)}
/>
{touched.password && errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
)}
</div>
{/* Password Strength Indicator */}
{password && (
<div style={{ marginBottom: '16px' }}>
<p style={{ fontSize: '12px', color: '#666' }}>
Password Strength:
<span style={{ color: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a' }}>
{password.length < 6 ? 'Weak' : password.length < 10 ? 'Medium' : 'Strong'}
</span>
</p>
<div style={{ height: '4px', backgroundColor: '#f0f0f0', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${Math.min(100, (password.length / 12) * 100)}%`,
backgroundColor: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a',
borderRadius: '2px',
transition: 'width 0.3s'
}} />
</div>
</div>
)}
<button type="submit" disabled={Object.keys(errors).length > 0}
style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length > 0 ? '#d9d9d9' : '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
function inputStyle(hasError) {
return {
width: '100%', padding: '8px',
border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px',
outline: 'none'
}
}
(2) Customize the useForm hook
Extract the form logic into a custom hook for easy reuse:
// ============================================
// Example:Custom useForm Hook
// Features:Unified Management of Form Status、Verification、Submit
// ============================================
function useForm(initialValues, validateFn) {
const [values, setValues] = React.useState(initialValues)
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({})
// Handling Input Changes
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newValues = { ...values, [name]: newValue }
setValues(newValues)
// Real-Time Verification
if (validateFn) {
const newErrors = validateFn(newValues)
setErrors(newErrors)
}
}
// Handling Loss of Focus
function handleBlur(e) {
const { name } = e.target
setTouched({ ...touched, [name]: true })
}
// Process Submission
function handleSubmit(callback) {
return (e) => {
e.preventDefault()
// Mark all fields as checked
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true
return acc
}, {})
setTouched(allTouched)
// Verification
if (validateFn) {
const newErrors = validateFn(values)
setErrors(newErrors)
if (Object.keys(newErrors).length > 0) return
}
callback(values)
}
}
// Reset Form
function reset() {
setValues(initialValues)
setErrors({})
setTouched({})
}
return { values, errors, touched, handleChange, handleBlur, handleSubmit, reset }
}
// ---- Usage useForm ----
function LoginForm() {
function validate(values) {
const errors = {}
if (!values.email) errors.email = 'Please enter your email address'
else if (!values.email.includes('@')) errors.email = 'The email address format is incorrect.'
if (!values.password) errors.password = 'Please enter your password'
else if (values.password.length < 6) errors.password = 'Password must be at least 6 chars'
return errors
}
const form = useForm({ email: '', password: '' }, validate)
return (
<form onSubmit={form.handleSubmit(data => console.log('Submit:', data))}
style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>Log In</h2>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Email"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.email && form.errors.email ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.email && form.errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.email}</p>
)}
<input
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Password"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.password && form.errors.password ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.password && form.errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.password}</p>
)}
<button type="submit" style={{
width: '100%', padding: '10px', backgroundColor: '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Log In
</button>
<button type="button" onClick={form.reset}
style={{ width: '100%', padding: '8px', marginTop: '8px', cursor: 'pointer' }}>
Reset
</button>
</form>
)
}
6. Complete Example: User Registration Form
// ============================================
// Complete Example:User Registration Form
// Features:Controlled Components + Real-Time Verification + Form Data Synchronization
// ============================================
function RegistrationForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
confirmPassword: '',
country: '',
gender: '',
acceptTerms: false
})
const [errors, setErrors] = React.useState({})
function validate() {
const newErrors = {}
if (!form.username || form.username.length < 2)
newErrors.username = 'Username must be at least 2 characters'
if (!form.email || !form.email.includes('@'))
newErrors.email = 'Please enter a valid email address'
if (!form.password || form.password.length < 6)
newErrors.password = 'Password must be at least 6 chars'
if (form.password !== form.confirmPassword)
newErrors.confirmPassword = 'The two passwords do not match'
if (!form.country)
newErrors.country = 'Please select a country'
if (!form.acceptTerms)
newErrors.acceptTerms = 'Please agree to the Terms of Service'
return newErrors
}
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newForm = { ...form, [name]: newValue }
setForm(newForm)
setErrors(validate())
// Interconnection:Automatically adjust password rule prompts after selecting a country
}
function handleSubmit(e) {
e.preventDefault()
const finalErrors = validate()
setErrors(finalErrors)
if (Object.keys(finalErrors).length === 0) {
alert(`Registration Successful!Welcome ${form.username}`)
}
}
function inputStyle(fieldName) {
return {
width: '100%', padding: '8px', marginBottom: '4px',
border: `1px solid ${errors[fieldName] ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px'
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📝 User Registration</h2>
<input name="username" value={form.username} onChange={handleChange}
placeholder="Username" style={inputStyle('username')} />
{errors.username && <p style={errorStyle}>{errors.username}</p>}
<input name="email" type="email" value={form.email} onChange={handleChange}
placeholder="Email" style={inputStyle('email')} />
{errors.email && <p style={errorStyle}>{errors.email}</p>}
<input name="password" type="password" value={form.password} onChange={handleChange}
placeholder="Password (at least 6 chars)" style={inputStyle('password')} />
{errors.password && <p style={errorStyle}>{errors.password}</p>}
<input name="confirmPassword" type="password" value={form.confirmPassword}
onChange={handleChange} placeholder="Confirm Password" style={inputStyle('confirmPassword')} />
{errors.confirmPassword && <p style={errorStyle}>{errors.confirmPassword}</p>}
<select name="country" value={form.country} onChange={handleChange}
style={inputStyle('country')}>
<option value="">-- Select a Country --</option>
<option value="CN">China</option>
<option value="US">United States</option>
<option value="JP">Japan</option>
<option value="BR">Brazil</option>
<option value="SA">Saudi Arabia</option>
</select>
{errors.country && <p style={errorStyle}>{errors.country}</p>}
<div style={{ margin: '12px 0' }}>
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '16px' }}>
<input type="radio" name="gender" value={g}
checked={form.gender === g} onChange={handleChange} />
{g}
</label>
))}
</div>
<label style={{ display: 'block', margin: '12px 0' }}>
<input type="checkbox" name="acceptTerms" checked={form.acceptTerms}
onChange={handleChange} />
I have read and agree to <a href="#">User Agreement</a>
</label>
{errors.acceptTerms && <p style={errorStyle}>{errors.acceptTerms}</p>}
<button type="submit" style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length === 0 && form.username ? '#1890ff' : '#d9d9d9',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
const errorStyle = {
color: '#ff4d4f', fontSize: '12px',
margin: '0 0 8px 0'
}
▶ Example 2: Form Data Binding—Automatically Filling in Area Codes When a Country Is Selected
Output:
Displays: "Please select". Input types: text, email, number. Dropdown: Please select, Beijing
function PhoneForm() {
const [form, setForm] = useState({ country: 'US', phone: '' })
const areaCodes = { US: '+1', CN: '+86', JP: '+81', UK: '+44', BR: '+55' }
function handleCountryChange(e) {
const country = e.target.value
setForm(prev => ({
...prev,
country,
phone: prev.phone.replace(/^\+\d+\s*/, '') + ' ',
}))
}
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Phone Contact</h3>
<select name="country" value={form.country} onChange={handleCountryChange}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
{Object.keys(areaCodes).map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
<div style={{ display: 'flex', gap: 8 }}>
<input value={areaCodes[form.country]} readOnly style={{ width: 60, padding: 8, borderRadius: 4 }} />
<input name="phone" value={form.phone} onChange={handleChange}
placeholder="Phone number" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
Full: {areaCodes[form.country]} {form.phone.trim()}
</p>
</div>
)
}
Output:
Country selector (US/CN/JP/UK/BR) → area code auto-fills (US: +1, CN: +86). Phone input + readonly area code. Shows "Full: +1 5551234"
▶ Example 3: Dynamic Form—Add/Remove Team Members
Output:
Displays: "Phone Contact". State: form (setter: setForm). Input: Phone number. Dropdown: {c}
function TeamForm() {
const [members, setMembers] = useState([{ name: '', email: '', role: 'developer' }])
function addMember() {
setMembers(prev => [...prev, { name: '', email: '', role: 'developer' }])
}
function removeMember(index) {
setMembers(prev => prev.filter((_, i) => i !== index))
}
function updateMember(index, field, value) {
setMembers(prev => prev.map((m, i) => i === index ? { ...m, [field]: value } : m))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Team:', members)
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '0 auto' }}>
<h3>Team Members</h3>
{members.map((m, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<input value={m.name} onChange={e => updateMember(i, 'name', e.target.value)}
placeholder="Name" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<input value={m.email} onChange={e => updateMember(i, 'email', e.target.value)}
placeholder="Email" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<select value={m.role} onChange={e => updateMember(i, 'role', e.target.value)}
style={{ padding: 6, borderRadius: 4 }}>
<option value="developer">Dev</option>
<option value="designer">Design</option>
<option value="manager">PM</option>
</select>
{members.length > 1 && (
<button type="button" onClick={() => removeMember(i)}
style={{ padding: '6px 10px', color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>
X
</button>
)}
</div>
))}
<button type="button" onClick={addMember}
style={{ padding: '6px 16px', marginRight: 8, cursor: 'pointer' }}>
+ Add Member
</button>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Submit</button>
</form>
)
}
Output:
Dynamic member rows (Name + Email + Role dropdown). "+ Add Member" adds row, "X" removes (min 1 kept). Submit → console.log("Team:", members)
▶ Example 4: Multi-Step Form Wizard
Output:
... ('Team:', members)
function MultiStepForm() {
const [step, setStep] = useState(1)
const [form, setForm] = useState({ name: '', email: '', password: '', city: '', agree: false })
const [errors, setErrors] = useState({})
function updateField(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
}
function validate() {
const errs = {}
if (step === 1) {
if (!form.name.trim()) errs.name = 'Name required'
if (!form.email.includes('@')) errs.email = 'Valid email required'
} else if (step === 2) {
if (form.password.length < 6) errs.password = 'At least 6 characters'
} else if (step === 3) {
if (!form.city) errs.city = 'Select a city'
if (!form.agree) errs.agree = 'Must agree to terms'
}
setErrors(errs)
return Object.keys(errs).length === 0
}
function next() { if (validate()) setStep(s => Math.min(s + 1, 3)) }
function back() { setStep(s => Math.max(s - 1, 1)) }
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Step {step} of 3</h3>
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
{[1, 2, 3].map(s => (
<div key={s} style={{ flex: 1, height: 4, borderRadius: 2,
backgroundColor: s <= step ? '#1890ff' : '#f0f0f0' }} />
))}
</div>
{step === 1 && (
<>
<input name="name" value={form.name} onChange={updateField} placeholder="Name"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.name ? '1px solid #ff4d4f' : undefined }} />
{errors.name && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.name}</p>}
<input name="email" value={form.email} onChange={updateField} placeholder="Email"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.email ? '1px solid #ff4d4f' : undefined }} />
{errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.email}</p>}
</>
)}
{step === 2 && (
<>
<input name="password" type="password" value={form.password} onChange={updateField}
placeholder="Password" style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }} />
{errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.password}</p>}
</>
)}
{step === 3 && (
<>
<select name="city" value={form.city} onChange={updateField}
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }}>
<option value="">Select city</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
</select>
{errors.city && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.city}</p>}
<label style={{ display: 'block', marginTop: 8 }}>
<input type="checkbox" name="agree" checked={form.agree} onChange={updateField} />
I agree to the terms
</label>
{errors.agree && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.agree}</p>}
</>
)}
<div style={{ marginTop: 16 }}>
{step > 1 && <button type="button" onClick={back} style={{ marginRight: 8, cursor: 'pointer' }}>Back</button>}
{step < 3 ? (
<button type="button" onClick={next} style={{ cursor: 'pointer' }}>Next</button>
) : (
<button type="button" onClick={() => { if (validate()) alert('Done!') }} style={{ cursor: 'pointer' }}>Submit</button>
)}
</div>
</div>
)
}
Output:
3-step wizard with progress bar. Step 1: Name+Email, Step 2: Password (6+ chars), Step 3: City+Terms. Next validates, Back returns. Submit → "Done!"
▶ Example 5: Unmanaged Components—File Upload and useRef
Output:
Displays "file upload (uncontrolled)". state: preview, filename. buttons: upload. uses useref
function FileUploadForm() {
const fileInputRef = useRef(null)
const [preview, setPreview] = useState(null)
const [fileName, setFileName] = useState('')
function handleSubmit(e) {
e.preventDefault()
const file = fileInputRef.current?.files[0]
if (!file) { alert('Please select a file'); return }
console.log('Uploading:', file.name, file.size, 'bytes')
alert(`File "${file.name}" (${(file.size / 1024).toFixed(1)} KB) selected for upload`)
}
function handleFileChange(e) {
const file = e.target.files[0]
if (!file) { setPreview(null); setFileName(''); return }
setFileName(file.name)
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>File Upload (Uncontrolled)</h3>
<div style={{ marginBottom: 12 }}>
<input type="file" ref={fileInputRef} onChange={handleFileChange}
accept="image/*,.pdf,.doc" style={{ marginBottom: 8 }} />
{fileName && <p style={{ fontSize: 13, color: '#666' }}>Selected: {fileName}</p>}
</div>
{preview && (
<div style={{ marginBottom: 12 }}>
<img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: 200, borderRadius: 4, border: '1px solid #eee' }} />
</div>
)}
<button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Upload</button>
</form>
)
}
Output:
File input → image preview shown, or filename displayed. Upload alerts "File "photo.jpg" (245.3 KB) selected for upload"
❓ FAQ
input[type="checkbox"], should I use checked or value?checked attribute (boolean), not value. To retrieve the new value in the corresponding onChange event, use e.target.checked. Similarly, for radio buttons, use checked.handleChange when a form has many fields?e.target.name in combination with the computed property name: const { name, value } = e.target; setForm(prev => ({...prev, [name]: value})). This way, all <input name="xxx"> can use the same handleChange. Checkboxes require special handling: const newValue = type === 'checkbox' ? checked : value.<input type="file"> You can only use uncontrolled components because their value is read-only. useForm Hooks can encapsulate boilerplate code for controlled components.📖 Summary
- Controlled components: The values of form elements are controlled by React state and updated via
onChange - 90% of the forms use controlled components; only
<input type="file">must use an uncontrolled component - Three points in time for form validation: onChange (in real time), onBlur (when the focus is lost), and onSubmit (upon submission)
- Reuse form processing logic using the generic
handleChange+nameproperties - A custom
useFormhook can encapsulate form logic for reuse
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create a
FeedbackFormcomponent that includes name, email, rating (dropdown options: Positive/Neutral/Negative), and content (textarea). When submitted, display all data in an alert. - Advanced Problem (Difficulty ⭐⭐): Create a
PasswordStrengthMetercomponent that displays password strength (weak/medium/strong) in real time. Rules: ≥8 characters + uppercase and lowercase letters + numbers = strong; ≥6 characters = medium; otherwise = weak. - Challenge (Difficulty: ⭐⭐⭐): Create a
DynamicFormcomponent that allows users to dynamically add or remove form items (e.g., "Add multiple family members"). Each form item should include three fields: name, age, and relationship. Upon submission, the component should output an array.