表单处理
表单是 Web 应用中最常见的用户交互方式之一。React 提供了两种处理表单的主要方式:受控组件和非受控组件。理解它们的区别和适用场景,对于构建高效、可维护的表单至关重要。
受控组件与非受控组件
核心概念对比
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React state | DOM 自身 |
| 数据更新 | 通过 onChange + setState | 通过 ref 直接访问 DOM |
| 值的验证 | 实时验证 | 提交时验证 |
| 适用场景 | 复杂表单、实时验证 | 简单表单、文件上传 |
| 代码复杂度 | 较高 | 较低 |
选择指南
- 使用受控组件:需要实时验证、条件禁用、动态表单字段、表单值需要与其他 UI 同步
- 使用非受控组件:文件上传、快速原型开发、集成非 React 代码、对性能有极致要求
受控组件详解
在受控组件中,表单数据完全由 React 组件的 state 控制。每个状态变更都有一个对应的事件处理器,这使得 React 能够成为"单一数据源"。
基本输入框
实时编辑器
function ControlledInput() { const [value, setValue] = useState(''); const handleChange = (e) => { setValue(e.target.value); }; return ( <div> <input type="text" value={value} onChange={handleChange} placeholder="输入内容..." style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }} /> <p style={{ fontSize: '14px', color: '#64748b' }}> 输入值: <strong style={{ color: '#3b82f6' }}>{value || '(空)'}</strong> </p> </div> ); }
结果
Loading...
多种表单控件
实时编辑器
function FormControlsDemo() { const [formData, setFormData] = useState({ text: '', textarea: '', select: 'apple', checkbox: false, radio: 'option1', number: 0, date: '', color: '#3b82f6' }); const handleChange = (field) => (e) => { const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; setFormData(prev => ({ ...prev, [field]: value })); }; return ( <div style={{ display: 'grid', gap: '15px' }}> {/* 文本输入 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>文本输入:</label> <input type="text" value={formData.text} onChange={handleChange('text')} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> {/* 多行文本 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>多行文本:</label> <textarea value={formData.textarea} onChange={handleChange('textarea')} rows={3} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> {/* 下拉选择 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>下拉选择:</label> <select value={formData.select} onChange={handleChange('select')} style={{ padding: '8px', width: '100%' }} > <option value="apple">苹果</option> <option value="banana">香蕉</option> <option value="orange">橙子</option> </select> </div> {/* 复选框 */} <div> <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <input type="checkbox" checked={formData.checkbox} onChange={handleChange('checkbox')} /> 同意用户协议 </label> </div> {/* 单选按钮组 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>单选按钮:</label> <div style={{ display: 'flex', gap: '15px' }}> {['option1', 'option2', 'option3'].map(opt => ( <label key={opt} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <input type="radio" name="radioGroup" value={opt} checked={formData.radio === opt} onChange={handleChange('radio')} /> 选项 {opt.slice(-1)} </label> ))} </div> </div> {/* 数字输入 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>数字输入:</label> <input type="number" value={formData.number} onChange={handleChange('number')} style={{ padding: '8px', width: '100px' }} /> </div> {/* 日期选择 */} <div> <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>日期选择:</label> <input type="date" value={formData.date} onChange={handleChange('date')} style={{ padding: '8px' }} /> </div> {/* 预览 */} <div style={{ marginTop: '10px', padding: '10px', background: '#f1f5f9', borderRadius: '4px' }}> <strong>当前表单数据:</strong> <pre style={{ margin: '5px 0', fontSize: '12px' }}>{JSON.stringify(formData, null, 2)}</pre> </div> </div> ); }
结果
Loading...
处理多个输入
当表单有多个字段时,可以为每个字段添加 name 属性,使用单一的事件处理器:
实时编辑器
function MultiFieldForm() { const [formData, setFormData] = useState({ username: '', email: '', password: '' }); // 统一的 change 处理器 const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleSubmit = (e) => { e.preventDefault(); alert(JSON.stringify(formData, null, 2)); }; return ( <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <div> <label style={{ display: 'block', marginBottom: '4px' }}>用户名:</label> <input name="username" value={formData.username} onChange={handleChange} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>邮箱:</label> <input name="email" type="email" value={formData.email} onChange={handleChange} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>密码:</label> <input name="password" type="password" value={formData.password} onChange={handleChange} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <button type="submit" style={{ padding: '10px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }}> 提交 </button> </form> ); }
结果
Loading...
非受控组件详解
非受控组件将数据存储在 DOM 中,而不是 React state 中。你可以使用 ref 来获取表单值。
基本用法
实时编辑器
function UncontrolledForm() { const inputRef = useRef(null); const textareaRef = useRef(null); const selectRef = useRef(null); const fileRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); const values = { input: inputRef.current?.value, textarea: textareaRef.current?.value, select: selectRef.current?.value, file: fileRef.current?.files?.[0]?.name }; alert(JSON.stringify(values, null, 2)); }; return ( <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <div> <label style={{ display: 'block', marginBottom: '4px' }}>文本输入:</label> <input ref={inputRef} defaultValue="默认值" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>多行文本:</label> <textarea ref={textareaRef} defaultValue="默认内容" rows={2} style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>下拉选择:</label> <select ref={selectRef} defaultValue="banana" style={{ padding: '8px' }}> <option value="apple">苹果</option> <option value="banana">香蕉</option> <option value="orange">橙子</option> </select> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>文件上传:</label> <input ref={fileRef} type="file" /> </div> <button type="submit" style={{ padding: '10px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }}> 提交 </button> </form> ); }
结果
Loading...
defaultValue 和 defaultChecked
对于非受控组件,可以使用 defaultValue 和 defaultChecked 设置初始值:
// 文本输入
<input defaultValue="初始值" />
// 复选框
<input type="checkbox" defaultChecked={true} />
// 单选按钮
<input type="radio" defaultChecked={true} />
// 下拉选择
<select defaultValue="option2">
<option value="option1">选项 1</option>
<option value="option2">选项 2</option>
</select>
文件输入
文件输入是典型的非受控组件场景,因为文件输入的值是只读的,只能通过用户操作设置。
实时编辑器
function FileUploadDemo() { const fileRef = useRef(null); const [preview, setPreview] = useState(null); const handleFileChange = () => { const file = fileRef.current?.files?.[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (e) => setPreview(e.target.result); reader.readAsDataURL(file); } else { setPreview(null); } }; return ( <div> <input ref={fileRef} type="file" accept="image/*" onChange={handleFileChange} style={{ marginBottom: '10px' }} /> {preview && ( <div> <img src={preview} alt="预览" style={{ maxWidth: '200px', maxHeight: '200px', borderRadius: '8px' }} /> </div> )} </div> ); }
结果
Loading...
表单验证
表单验证是表单处理中的重要环节。我们可以在客户端进行基本的验证,提高用户体验。
实时验证
实时编辑器
function RealTimeValidation() { const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' }); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); // 验证规则 const validate = (name, value) => { switch (name) { case 'username': if (!value) return '用户名不能为空'; if (value.length < 3) return '用户名至少 3 个字符'; if (!/^[a-zA-Z0-9_]+$/.test(value)) return '只能包含字母、数字和下划线'; return ''; case 'email': if (!value) return '邮箱不能为空'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '邮箱格式不正确'; return ''; case 'password': if (!value) return '密码不能为空'; if (value.length < 8) return '密码至少 8 个字符'; if (!/[A-Z]/.test(value)) return '需要包含大写字母'; if (!/[0-9]/.test(value)) return '需要包含数字'; return ''; case 'confirmPassword': if (!value) return '请确认密码'; if (value !== formData.password) return '两次密码不一致'; return ''; default: return ''; } }; const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // 实时验证 const error = validate(name, value); setErrors(prev => ({ ...prev, [name]: error })); }; const handleBlur = (e) => { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); }; const handleSubmit = (e) => { e.preventDefault(); // 验证所有字段 const newErrors = {}; Object.keys(formData).forEach(key => { newErrors[key] = validate(key, formData[key]); }); setErrors(newErrors); setTouched({ username: true, email: true, password: true, confirmPassword: true }); // 检查是否有错误 if (Object.values(newErrors).every(err => !err)) { alert('注册成功!\n' + JSON.stringify(formData, null, 2)); } }; const inputStyle = (name) => ({ padding: '10px', width: '100%', boxSizing: 'border-box', border: touched[name] && errors[name] ? '2px solid #ef4444' : '1px solid #e2e8f0', borderRadius: '4px' }); return ( <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> <div> <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>用户名:</label> <input name="username" value={formData.username} onChange={handleChange} onBlur={handleBlur} placeholder="3个字符以上,字母数字下划线" style={inputStyle('username')} /> {touched.username && errors.username && ( <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.username}</p> )} </div> <div> <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>邮箱:</label> <input name="email" type="email" value={formData.email} onChange={handleChange} onBlur={handleBlur} placeholder="[email protected]" style={inputStyle('email')} /> {touched.email && errors.email && ( <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p> )} </div> <div> <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>密码:</label> <input name="password" type="password" value={formData.password} onChange={handleChange} onBlur={handleBlur} placeholder="至少8位,包含大写字母和数字" style={inputStyle('password')} /> {touched.password && errors.password && ( <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p> )} </div> <div> <label style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>确认密码:</label> <input name="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleChange} onBlur={handleBlur} placeholder="再次输入密码" style={inputStyle('confirmPassword')} /> {touched.confirmPassword && errors.confirmPassword && ( <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0' }}>{errors.confirmPassword}</p> )} </div> <button type="submit" style={{ padding: '12px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }} > 注册 </button> </form> ); }
结果
Loading...
密码强度指示器
实时编辑器
function PasswordStrength() { const [password, setPassword] = useState(''); const calculateStrength = (pwd) => { let score = 0; if (pwd.length >= 8) score++; if (pwd.length >= 12) score++; if (/[a-z]/.test(pwd)) score++; if (/[A-Z]/.test(pwd)) score++; if (/[0-9]/.test(pwd)) score++; if (/[^a-zA-Z0-9]/.test(pwd)) score++; return Math.min(score, 5); }; const strength = calculateStrength(password); const labels = ['非常弱', '弱', '一般', '强', '非常强']; const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#10b981']; return ( <div> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="输入密码查看强度" style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }} /> {password && ( <div style={{ marginTop: '10px' }}> <div style={{ display: 'flex', gap: '4px' }}> {[0, 1, 2, 3, 4].map(i => ( <div key={i} style={{ flex: 1, height: '6px', borderRadius: '3px', background: i < strength ? colors[strength - 1] : '#e2e8f0' }} /> ))} </div> <p style={{ margin: '5px 0', fontSize: '12px', color: colors[strength - 1] || '#64748b' }}> 密码强度: {labels[strength - 1] || '非常弱'} </p> </div> )} </div> ); }
结果
Loading...
React 19 表单 Actions
React 19 引入了 Actions 概念,极大简化了表单处理。你可以在 <form> 的 action 属性中直接传递函数。
useActionState
useActionState 用于管理表单提交的状态。
实时编辑器
function ActionFormDemo() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { // 模拟 API 调用 await new Promise(resolve => setTimeout(resolve, 1000)); const name = formData.get('name'); const email = formData.get('email'); if (!name) { return { error: '请输入姓名' }; } if (!email) { return { error: '请输入邮箱' }; } return { success: true, message: `欢迎, ${name}!` }; }, null ); return ( <form action={formAction} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <div> <label style={{ display: 'block', marginBottom: '4px' }}>姓名:</label> <input name="name" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <div> <label style={{ display: 'block', marginBottom: '4px' }}>邮箱:</label> <input name="email" type="email" style={{ padding: '8px', width: '100%', boxSizing: 'border-box' }} /> </div> <button type="submit" disabled={isPending} style={{ padding: '10px', background: isPending ? '#94a3b8' : '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: isPending ? 'not-allowed' : 'pointer' }} > {isPending ? '提交中...' : '提交'} </button> {state?.error && ( <p style={{ color: '#ef4444', fontSize: '14px' }}>{state.error}</p> )} {state?.success && ( <p style={{ color: '#22c55e', fontSize: '14px' }}>{state.message}</p> )} </form> ); }
结果
Loading...
useOptimistic 乐观更新
乐观更新是一种在等待服务器响应时就先更新 UI 的技术,提升用户体验。
实时编辑器
function OptimisticLikeDemo() { const [likes, setLikes] = useState(42); const [optimisticLikes, addOptimisticLike] = useOptimistic( likes, (currentLikes) => currentLikes + 1 ); async function handleLike() { addOptimisticLike(null); // 模拟 API 调用 await new Promise(resolve => setTimeout(resolve, 1000)); setLikes(prev => prev + 1); } return ( <div style={{ textAlign: 'center', padding: '20px' }}> <button onClick={handleLike} style={{ padding: '15px 30px', fontSize: '18px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }} > ❤️ {optimisticLikes} 赞 </button> <p style={{ fontSize: '12px', color: '#64748b', marginTop: '10px' }}> {optimisticLikes !== likes ? '更新中...' : '点击点赞(乐观更新演示)'} </p> </div> ); }
结果
Loading...
复杂表单模式
动态表单字段
实时编辑器
function DynamicFormFields() { const [fields, setFields] = useState([ { id: 1, name: '', email: '' } ]); const addField = () => { setFields(prev => [ ...prev, { id: Date.now(), name: '', email: '' } ]); }; const removeField = (id) => { if (fields.length > 1) { setFields(prev => prev.filter(f => f.id !== id)); } }; const updateField = (id, field, value) => { setFields(prev => prev.map(f => f.id === id ? { ...f, [field]: value } : f )); }; const handleSubmit = (e) => { e.preventDefault(); alert(JSON.stringify(fields, null, 2)); }; return ( <form onSubmit={handleSubmit}> {fields.map((field, index) => ( <div key={field.id} style={{ display: 'flex', gap: '10px', marginBottom: '10px', padding: '10px', background: '#f8fafc', borderRadius: '4px' }}> <span style={{ display: 'flex', alignItems: 'center', width: '24px', color: '#64748b' }}> #{index + 1} </span> <input placeholder="姓名" value={field.name} onChange={(e) => updateField(field.id, 'name', e.target.value)} style={{ padding: '8px', flex: 1 }} /> <input placeholder="邮箱" value={field.email} onChange={(e) => updateField(field.id, 'email', e.target.value)} style={{ padding: '8px', flex: 1 }} /> <button type="button" onClick={() => removeField(field.id)} style={{ padding: '8px 12px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 删除 </button> </div> ))} <div style={{ display: 'flex', gap: '10px' }}> <button type="button" onClick={addField} style={{ padding: '10px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }} > + 添加字段 </button> <button type="submit" style={{ padding: '10px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 提交表单 </button> </div> </form> ); }
结果
Loading...
多步骤表单
实时编辑器
function MultiStepForm() { const [step, setStep] = useState(1); const [formData, setFormData] = useState({ // 步骤 1: 个人信息 firstName: '', lastName: '', // 步骤 2: 联系方式 email: '', phone: '', // 步骤 3: 地址 address: '', city: '' }); const handleChange = (e) => { setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); }; const nextStep = () => setStep(s => Math.min(s + 1, 3)); const prevStep = () => setStep(s => Math.max(s - 1, 1)); const handleSubmit = () => { alert('表单提交成功!\n' + JSON.stringify(formData, null, 2)); }; const renderStep = () => { switch (step) { case 1: return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <h4>步骤 1: 个人信息</h4> <input name="firstName" placeholder="名" value={formData.firstName} onChange={handleChange} style={{ padding: '10px' }} /> <input name="lastName" placeholder="姓" value={formData.lastName} onChange={handleChange} style={{ padding: '10px' }} /> </div> ); case 2: return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <h4>步骤 2: 联系方式</h4> <input name="email" type="email" placeholder="邮箱" value={formData.email} onChange={handleChange} style={{ padding: '10px' }} /> <input name="phone" placeholder="电话" value={formData.phone} onChange={handleChange} style={{ padding: '10px' }} /> </div> ); case 3: return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <h4>步骤 3: 地址</h4> <input name="address" placeholder="详细地址" value={formData.address} onChange={handleChange} style={{ padding: '10px' }} /> <input name="city" placeholder="城市" value={formData.city} onChange={handleChange} style={{ padding: '10px' }} /> </div> ); } }; return ( <div> {/* 进度指示器 */} <div style={{ display: 'flex', marginBottom: '20px' }}> {[1, 2, 3].map(s => ( <div key={s} style={{ flex: 1, padding: '10px', textAlign: 'center', background: s <= step ? '#3b82f6' : '#e2e8f0', color: s <= step ? 'white' : '#64748b', marginRight: s < 3 ? '2px' : 0 }} > {s} </div> ))} </div> {/* 表单内容 */} {renderStep()} {/* 导航按钮 */} <div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}> {step > 1 && ( <button type="button" onClick={prevStep} style={{ padding: '10px 20px' }} > 上一步 </button> )} {step < 3 ? ( <button type="button" onClick={nextStep} style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 下一步 </button> ) : ( <button type="button" onClick={handleSubmit} style={{ padding: '10px 20px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }} > 提交 </button> )} </div> </div> ); }
结果
Loading...
表单库推荐
对于复杂的表单需求,推荐使用成熟的表单库:
react-hook-form
react-hook-form 是一个高性能、灵活的表单库,具有以下特点:
- 减少不必要的重新渲染
- 最小化的重新渲染
- 内置验证支持
- 与 UI 库良好集成
import { useForm } from 'react-hook-form';
function MyForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm();
const onSubmit = async (data) => {
await submitForm(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: '邮箱必填',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '邮箱格式不正确'
}
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<button type="submit" disabled={isSubmitting}>
提交
</button>
</form>
);
}
小结
- 受控组件:表单数据由 React state 控制,适合复杂表单和实时验证
- 非受控组件:使用 ref 获取表单值,适合简单场景和文件上传
- 表单验证:实时验证、提交验证和密码强度检查
- React 19 Actions:useActionState 和 useOptimistic 简化表单处理
- 复杂表单:动态字段、多步骤表单等高级模式
- 表单库:react-hook-form 等库可以简化复杂表单开发
练习
- 实现一个包含实时验证的登录表单
- 创建一个多步骤注册表单,包含进度指示
- 实现一个动态添加/删除字段的表单
- 使用 React 19 的 useActionState 重构一个现有表单
- 实现一个带有乐观更新的评论提交表单