Appearance
受控组件详解
学习目标
通过本章学习,你将全面掌握:
- 受控组件的概念和原理
- 各种表单元素的受控实现
- 受控组件的优势和应用场景
- 表单验证的完整流程
- 性能优化策略
- 常见问题和解决方案
- React 19中的表单处理新特性
第一部分:受控组件基础
1.1 什么是受控组件
受控组件是指表单元素的值完全由React State控制的组件。
jsx
// 受控组件的定义
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value} // 值由State控制
onChange={e => setValue(e.target.value)} // 通过事件更新State
/>
);
}
// 非受控组件(对比)
function UncontrolledInput() {
return (
<input defaultValue="初始值" /> // 值由DOM自己管理
);
}
// 为什么叫"受控"?
// 因为值完全受React控制,DOM只是展示1.2 受控组件的工作流程
jsx
function ControlledFlow() {
const [value, setValue] = useState('Hello');
// 数据流:
// 1. State (value = "Hello")
// 2. ↓ 渲染到DOM
// 3. <input value="Hello" />
// 4. 用户输入 "Hi"
// 5. onChange事件触发
// 6. setValue("Hi")
// 7. State更新 (value = "Hi")
// 8. 重新渲染
// 9. <input value="Hi" />
const handleChange = (e) => {
const newValue = e.target.value;
console.log('旧值:', value);
console.log('新值:', newValue);
setValue(newValue);
};
return (
<div>
<input value={value} onChange={handleChange} />
<p>当前值: {value}</p>
</div>
);
}1.3 受控组件的优势
jsx
function ControlledAdvantages() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
// 优势1:实时验证
const usernameError = username && username.length < 3 ? '用户名至少3个字符' : '';
// 优势2:格式化输入
const handleUsernameChange = (e) => {
let value = e.target.value;
// 自动转小写
value = value.toLowerCase();
// 只允许字母数字和下划线
value = value.replace(/[^a-z0-9_]/g, '');
// 限制长度
if (value.length <= 20) {
setUsername(value);
}
};
// 优势3:条件禁用
const handlePasswordChange = (e) => {
const value = e.target.value;
// 只在用户名有效时才允许输入密码
if (username.length >= 3) {
setPassword(value);
}
};
// 优势4:实时比较
const passwordMatch = password && confirmPassword && password === confirmPassword;
const passwordMismatch = password && confirmPassword && password !== confirmPassword;
// 优势5:动态提示
const getPasswordStrength = () => {
if (!password) return '';
if (password.length < 6) return '弱';
if (password.length < 10) return '中等';
return '强';
};
return (
<form>
<div>
<label>用户名:</label>
<input
value={username}
onChange={handleUsernameChange}
placeholder="3-20个字符"
/>
{usernameError && <span className="error">{usernameError}</span>}
<span className="info">{username.length}/20</span>
</div>
<div>
<label>密码:</label>
<input
type="password"
value={password}
onChange={handlePasswordChange}
disabled={username.length < 3}
placeholder="至少6个字符"
/>
{password && (
<span className="strength">强度: {getPasswordStrength()}</span>
)}
</div>
<div>
<label>确认密码:</label>
<input
type="password"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
placeholder="再次输入密码"
/>
{passwordMatch && <span className="success">✓ 密码匹配</span>}
{passwordMismatch && <span className="error">✗ 密码不匹配</span>}
</div>
<button
type="submit"
disabled={!username || !password || !passwordMatch}
>
注册
</button>
</form>
);
}第二部分:各种表单元素的受控实现
2.1 input文本框
jsx
function ControlledTextInputs() {
const [text, setText] = useState('');
const [number, setNumber] = useState(0);
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [email, setEmail] = useState('');
const [url, setUrl] = useState('');
const [search, setSearch] = useState('');
const [tel, setTel] = useState('');
const [color, setColor] = useState('#000000');
const [range, setRange] = useState(50);
return (
<div className="form-grid">
<div>
<label>text:</label>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
placeholder="文本"
/>
</div>
<div>
<label>number:</label>
<input
type="number"
value={number}
onChange={e => setNumber(Number(e.target.value))}
min="0"
max="100"
/>
</div>
<div>
<label>date:</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
/>
</div>
<div>
<label>time:</label>
<input
type="time"
value={time}
onChange={e => setTime(e.target.value)}
/>
</div>
<div>
<label>email:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<label>url:</label>
<input
type="url"
value={url}
onChange={e => setUrl(e.target.value)}
/>
</div>
<div>
<label>search:</label>
<input
type="search"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div>
<label>tel:</label>
<input
type="tel"
value={tel}
onChange={e => setTel(e.target.value)}
/>
</div>
<div>
<label>color:</label>
<input
type="color"
value={color}
onChange={e => setColor(e.target.value)}
/>
</div>
<div>
<label>range ({range}):</label>
<input
type="range"
value={range}
onChange={e => setRange(Number(e.target.value))}
min="0"
max="100"
/>
</div>
</div>
);
}2.2 textarea
jsx
function ControlledTextarea() {
const [content, setContent] = useState('');
const [wordCount, setWordCount] = useState(0);
const [charCount, setCharCount] = useState(0);
const maxLength = 500;
const handleChange = (e) => {
const value = e.target.value;
if (value.length <= maxLength) {
setContent(value);
setCharCount(value.length);
setWordCount(value.trim().split(/\s+/).filter(Boolean).length);
}
};
const remaining = maxLength - charCount;
const progress = (charCount / maxLength) * 100;
return (
<div>
<textarea
value={content}
onChange={handleChange}
placeholder="请输入内容..."
rows={10}
cols={50}
/>
<div className="textarea-info">
<span>字符: {charCount}/{maxLength}</span>
<span>单词: {wordCount}</span>
<span>剩余: {remaining}</span>
<div className="progress-bar">
<div
className="progress-fill"
style={{
width: `${progress}%`,
background: progress > 90 ? '#e74c3c' : '#28a745'
}}
/>
</div>
</div>
</div>
);
}2.3 select下拉框
jsx
function ControlledSelect() {
const [single, setSingle] = useState('');
const [multiple, setMultiple] = useState([]);
// 单选
const handleSingleChange = (e) => {
setSingle(e.target.value);
};
// 多选
const handleMultipleChange = (e) => {
const options = Array.from(e.target.selectedOptions);
const values = options.map(opt => opt.value);
setMultiple(values);
};
return (
<div>
<div>
<label>单选下拉框:</label>
<select value={single} onChange={handleSingleChange}>
<option value="">请选择</option>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
<option value="orange">橙子</option>
</select>
<p>选择: {single || '未选择'}</p>
</div>
<div>
<label>多选下拉框:</label>
<select
multiple
value={multiple}
onChange={handleMultipleChange}
size={5}
>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
<option value="orange">橙子</option>
<option value="grape">葡萄</option>
<option value="watermelon">西瓜</option>
</select>
<p>选择: {multiple.join(', ') || '未选择'}</p>
</div>
</div>
);
}2.4 checkbox复选框
jsx
function ControlledCheckbox() {
const [checked, setChecked] = useState(false);
const [selected, setSelected] = useState([]);
const options = ['阅读', '运动', '音乐', '旅行', '摄影'];
// 单个checkbox
const handleSingleCheck = (e) => {
setChecked(e.target.checked);
};
// 多个checkbox
const handleOptionChange = (option) => {
setSelected(prev =>
prev.includes(option)
? prev.filter(item => item !== option)
: [...prev, option]
);
};
// 全选/取消全选
const toggleAll = () => {
setSelected(prev =>
prev.length === options.length ? [] : [...options]
);
};
const allSelected = selected.length === options.length;
const someSelected = selected.length > 0 && selected.length < options.length;
return (
<div>
<div>
<label>
<input
type="checkbox"
checked={checked}
onChange={handleSingleCheck}
/>
同意条款
</label>
</div>
<div>
<h4>选择爱好:</h4>
<label>
<input
type="checkbox"
checked={allSelected}
ref={input => {
if (input) {
input.indeterminate = someSelected;
}
}}
onChange={toggleAll}
/>
全选
</label>
{options.map(option => (
<label key={option}>
<input
type="checkbox"
checked={selected.includes(option)}
onChange={() => handleOptionChange(option)}
/>
{option}
</label>
))}
<p>已选择: {selected.join(', ') || '无'}</p>
</div>
</div>
);
}2.5 radio单选框
jsx
function ControlledRadio() {
const [selected, setSelected] = useState('');
const [paymentMethod, setPaymentMethod] = useState('alipay');
const options = ['苹果', '香蕉', '橙子'];
const paymentMethods = [
{ id: 'alipay', name: '支付宝', icon: '💰' },
{ id: 'wechat', name: '微信支付', icon: '💚' },
{ id: 'card', name: '银行卡', icon: '💳' }
];
return (
<div>
<div>
<h4>选择水果:</h4>
{options.map(option => (
<label key={option}>
<input
type="radio"
name="fruit"
value={option}
checked={selected === option}
onChange={e => setSelected(e.target.value)}
/>
{option}
</label>
))}
<p>选择: {selected || '未选择'}</p>
</div>
<div>
<h4>支付方式:</h4>
<div className="payment-methods">
{paymentMethods.map(method => (
<label
key={method.id}
className={`payment-option ${paymentMethod === method.id ? 'selected' : ''}`}
>
<input
type="radio"
name="payment"
value={method.id}
checked={paymentMethod === method.id}
onChange={e => setPaymentMethod(e.target.value)}
/>
<span className="icon">{method.icon}</span>
<span className="name">{method.name}</span>
</label>
))}
</div>
</div>
</div>
);
}第三部分:表单验证
3.1 实时验证
jsx
function RealTimeValidation() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// 验证规则
const validators = {
username: (value) => {
if (!value) return '用户名不能为空';
if (value.length < 3) return '用户名至少3个字符';
if (value.length > 20) return '用户名最多20个字符';
if (!/^[a-zA-Z0-9_]+$/.test(value)) return '只能包含字母、数字和下划线';
return '';
},
email: (value) => {
if (!value) return '邮箱不能为空';
if (!/\S+@\S+\.\S+/.test(value)) return '邮箱格式不正确';
return '';
},
password: (value) => {
if (!value) return '密码不能为空';
if (value.length < 8) return '密码至少8个字符';
if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
if (!/[a-z]/.test(value)) return '密码必须包含小写字母';
if (!/[0-9]/.test(value)) return '密码必须包含数字';
if (!/[!@#$%^&*]/.test(value)) return '密码必须包含特殊字符';
return '';
},
confirmPassword: (value) => {
if (!value) return '请确认密码';
if (value !== formData.password) return '两次密码不一致';
return '';
}
};
// 处理变化
const handleChange = (field) => (e) => {
const value = e.target.value;
setFormData(prev => ({
...prev,
[field]: value
}));
// 如果字段已被访问过,立即验证
if (touched[field]) {
const error = validators[field](value);
setErrors(prev => ({
...prev,
[field]: error
}));
}
};
// 处理失焦
const handleBlur = (field) => () => {
setTouched(prev => ({
...prev,
[field]: true
}));
const error = validators[field](formData[field]);
setErrors(prev => ({
...prev,
[field]: error
}));
};
// 提交验证
const handleSubmit = (e) => {
e.preventDefault();
// 标记所有字段为已访问
const allTouched = {};
Object.keys(formData).forEach(key => {
allTouched[key] = true;
});
setTouched(allTouched);
// 验证所有字段
const allErrors = {};
Object.keys(formData).forEach(field => {
const error = validators[field](formData[field]);
if (error) {
allErrors[field] = error;
}
});
setErrors(allErrors);
// 检查是否有错误
if (Object.keys(allErrors).length === 0) {
console.log('提交数据:', formData);
alert('注册成功!');
}
};
return (
<form onSubmit={handleSubmit} className="validation-form">
<div className="form-field">
<label>用户名:</label>
<input
value={formData.username}
onChange={handleChange('username')}
onBlur={handleBlur('username')}
className={touched.username && errors.username ? 'error' : ''}
/>
{touched.username && errors.username && (
<span className="error-message">{errors.username}</span>
)}
</div>
<div className="form-field">
<label>邮箱:</label>
<input
type="email"
value={formData.email}
onChange={handleChange('email')}
onBlur={handleBlur('email')}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<span className="error-message">{errors.email}</span>
)}
</div>
<div className="form-field">
<label>密码:</label>
<input
type="password"
value={formData.password}
onChange={handleChange('password')}
onBlur={handleBlur('password')}
className={touched.password && errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<span className="error-message">{errors.password}</span>
)}
</div>
<div className="form-field">
<label>确认密码:</label>
<input
type="password"
value={formData.confirmPassword}
onChange={handleChange('confirmPassword')}
onBlur={handleBlur('confirmPassword')}
className={touched.confirmPassword && errors.confirmPassword ? 'error' : ''}
/>
{touched.confirmPassword && errors.confirmPassword && (
<span className="error-message">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">注册</button>
</form>
);
}3.2 异步验证
jsx
function AsyncValidation() {
const [username, setUsername] = useState('');
const [checking, setChecking] = useState(false);
const [available, setAvailable] = useState(null);
// 防抖检查用户名
useEffect(() => {
if (!username || username.length < 3) {
setAvailable(null);
return;
}
setChecking(true);
const timer = setTimeout(async () => {
try {
const response = await fetch(`/api/check-username?username=${username}`);
const data = await response.json();
setAvailable(data.available);
} catch (error) {
console.error('检查失败', error);
} finally {
setChecking(false);
}
}, 500);
return () => clearTimeout(timer);
}, [username]);
return (
<div>
<label>用户名:</label>
<input
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="输入用户名"
/>
{checking && <span className="checking">检查中...</span>}
{!checking && available === true && (
<span className="success">✓ 用户名可用</span>
)}
{!checking && available === false && (
<span className="error">✗ 用户名已被使用</span>
)}
</div>
);
}第四部分:性能优化
4.1 大型表单优化
jsx
// 问题:每个字段变化都会重新渲染整个表单
function UnoptimizedLargeForm() {
const [formData, setFormData] = useState({
field1: '', field2: '', field3: '', /* ...100个字段 */
});
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
};
console.log('Form渲染'); // 每次输入都渲染
return (
<form>
{/* 100个字段... */}
</form>
);
}
// 解决方案:拆分为独立组件
const FormField = React.memo(function FormField({
label,
value,
onChange,
error
}) {
console.log('FormField渲染:', label);
return (
<div className="form-field">
<label>{label}</label>
<input value={value} onChange={onChange} />
{error && <span className="error">{error}</span>}
</div>
);
});
function OptimizedLargeForm() {
const [field1, setField1] = useState('');
const [field2, setField2] = useState('');
const [field3, setField3] = useState('');
// 每个字段独立的State
return (
<form>
<FormField label="字段1" value={field1} onChange={e => setField1(e.target.value)} />
<FormField label="字段2" value={field2} onChange={e => setField2(e.target.value)} />
<FormField label="字段3" value={field3} onChange={e => setField3(e.target.value)} />
{/* field1变化时,只有FormField1重新渲染 */}
</form>
);
}第五部分:React 19新特性
5.1 Server Actions表单
jsx
'use server';
async function createUser(formData) {
const data = {
name: formData.get('name'),
email: formData.get('email')
};
await db.users.create(data);
revalidatePath('/users');
return { success: true };
}
// Client Component
'use client';
import { useActionState } from 'react';
function ServerActionForm() {
const [state, formAction, isPending] = useActionState(createUser, null);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
{state?.success && <p>创建成功!</p>}
</form>
);
}练习题
基础练习
- 实现各种受控表单元素
- 创建实时表单验证
- 实现多个表单元素的联动
进阶练习
- 创建复杂的表单验证系统
- 实现表单的自动保存功能
- 优化大型表单性能
高级练习
- 使用React 19 Server Actions
- 实现表单的撤销/重做
- 创建动态表单生成器
通过本章学习,你已经掌握了受控组件的完整知识。受控组件是React表单处理的核心,掌握它对构建复杂表单至关重要!继续学习,成为表单处理专家!