学习路径
中级
表单高级处理
深入学习 React 表单处理,包括受控组件、表单验证、以及 React 19 的 Actions
表单处理概述
表单是 Web 应用中最重要的交互元素之一。React 提供了多种处理表单的方式,从基础的受控组件到 React 19 的 Actions。本章将深入探讨:
- 受控组件 vs 非受控组件
- 表单状态管理
- 表单验证和错误处理
- React 19 的 Actions 和 useActionState
- 表单性能优化
- 复杂表单场景
受控组件
在 React 中,表单元素通常使用受控组件来处理。受控组件的值由 React 状态控制,并通过事件处理函数更新。
基础受控组件
TypeScript
import { useState } from 'react';
function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log({ name, email, message });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">姓名:</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email">邮箱:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="message">留言:</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<button type="submit">提交</button>
</form>
);
}
为什么使用受控组件?
- 即时验证: 可以在用户输入时验证数据
- 条件禁用: 根据输入值动态启用/禁用按钮
- 格式化输入: 自动格式化用户输入(如电话号码)
- 单数据源: 表单状态由 React 管理,易于调试
使用单个状态对象
TypeScript
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
/>
</form>
);
}
表单验证
表单验证是确保用户输入数据正确性的关键。我们可以实现实时验证和提交时验证。
实时验证
TypeScript
import { useState, useEffect } from 'react';
interface ValidationResult {
isValid: boolean;
errors: {
name?: string;
email?: string;
message?: string;
};
}
function validateForm(data: { name: string; email: string; message: string }): ValidationResult {
const errors: ValidationResult['errors'] = {};
try {
if (!data.name.trim()) {
errors.name = '姓名不能为空';
} else if (data.name.length < 2) {
errors.name = '姓名至少2个字符';
}
if (!data.email.trim()) {
errors.email = '邮箱不能为空';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = '邮箱格式不正确';
}
if (!data.message.trim()) {
errors.message = '留言不能为空';
} else if (data.message.length < 10) {
errors.message = '留言至少10个字符';
}
} catch (error) {
// 验证函数不应该抛出异常,但为了健壮性,捕获并处理
console.error('验证过程出错:', error);
errors.name = '验证出错,请重试';
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
function ValidatedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const [validation, setValidation] = useState<ValidationResult>({
isValid: false,
errors: {}
});
// 实时验证(带防抖优化)
useEffect(() => {
const timer = setTimeout(() => {
const result = validateForm(formData);
setValidation(result);
}, 300); // 300ms 防抖延迟
return () => clearTimeout(timer);
}, [formData]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { name } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 标记所有字段为已触摸
setTouched({
name: true,
email: true,
message: true
});
if (validation.isValid) {
console.log('提交数据:', formData);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name">姓名:</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
className={touched.name && validation.errors.name ? 'border-red-500' : ''}
/>
{touched.name && validation.errors.name && (
<span className="text-red-500 text-sm">
{validation.errors.name}
</span>
)}
</div>
<div>
<label htmlFor="email">邮箱:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && validation.errors.email ? 'border-red-500' : ''}
/>
{touched.email && validation.errors.email && (
<span className="text-red-500 text-sm">
{validation.errors.email}
</span>
)}
</div>
<div>
<label htmlFor="message">留言:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
className={touched.message && validation.errors.message ? 'border-red-500' : ''}
/>
{touched.message && validation.errors.message && (
<span className="text-red-500 text-sm">
{validation.errors.message}
</span>
)}
</div>
<button
type="submit"
disabled={!validation.isValid}
className={!validation.isValid ? 'opacity-50 cursor-not-allowed' : ''}
>
提交
</button>
</form>
);
}
用户体验最佳实践
- 不要在用户开始输入前显示错误 - 使用 touched 状态
- 提供明确的错误信息 - 告诉用户如何修复
- 禁用提交按钮 - 直到表单有效
- 显示加载状态 - 提交时禁用表单并显示 spinner
- 使用防抖 - 避免频繁触发验证导致性能问题
错误处理的健壮性
表单验证的常见问题:
- 验证函数可能抛出异常:使用 try-catch 包裹验证逻辑
- 频繁验证影响性能:使用防抖(debounce)延迟验证
- 异步验证的竞态条件:使用 AbortController 取消未完成的验证
- 边界情况处理:空值、null、undefined 等情况要妥善处理
性能建议:对于实时验证,至少使用 300ms 的防抖延迟。 如果验证涉及网络请求(如检查用户名是否存在),考虑使用更长的延迟(500ms)。
异步验证(带错误处理)
TypeScript
import { useState, useEffect } from 'react';
function UsernameForm() {
const [username, setUsername] = useState('');
const [validationStatus, setValidationStatus] = useState<{
isValid: boolean;
message: string;
checking: boolean;
}>({
isValid: true,
message: '',
checking: false
});
useEffect(() => {
// 防抖 + AbortController
const controller = new AbortController();
const timer = setTimeout(async () => {
if (username.length < 3) return;
setValidationStatus(prev => ({ ...prev, checking: true }));
try {
const response = await fetch(`/api/check-username?username=${username}`, {
signal: controller.signal
});
if (!response.ok) {
throw new Error('验证失败');
}
const data = await response.json();
setValidationStatus({
isValid: data.available,
message: data.available ? '用户名可用' : '用户名已被占用',
checking: false
});
} catch (error) {
if (error.name !== 'AbortError') {
setValidationStatus({
isValid: false,
message: '验证出错,请重试',
checking: false
});
}
}
}, 500); // 500ms 防抖
return () => {
clearTimeout(timer);
controller.abort();
};
}, [username]);
return (
<div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
{username.length >= 3 && (
<div>
{validationStatus.checking && <span>检查中...</span>}
{!validationStatus.checking && validationStatus.message && (
<span className={validationStatus.isValid ? 'text-green-500' : 'text-red-500'}>
{validationStatus.message}
</span>
)}
</div>
)}
</div>
);
}
React 19 Actions
React 19 引入了 Actions,简化了表单提交和数据变更的处理。Actions 可以是函数,自动处理加载状态、错误和乐观更新。
基础 Action
TypeScript
// 定义 Action
async function updateName(formData: FormData) {
const name = formData.get('name');
await updateUser({ name });
return { success: true };
}
// 在表单中使用
function NameForm() {
return (
<form action={updateName}>
<input type="text" name="name" />
<button type="submit">更新</button>
</form>
);
}
使用 useActionState
TypeScript
import { useActionState } from 'react';
interface FormState {
success: boolean;
error: string | null;
data: null | { name: string };
}
async function submitForm(prevState: FormState, formData: FormData) {
const name = formData.get('name') as string;
if (!name || name.length < 2) {
return {
success: false,
error: '姓名至少2个字符',
data: null
};
}
try {
await updateUser({ name });
return {
success: true,
error: null,
data: { name }
};
} catch (error) {
return {
success: false,
error: '更新失败,请重试',
data: null
};
}
}
function UserForm() {
const [state, formAction, isPending] = useActionState(submitForm, {
success: false,
error: null,
data: null
});
return (
<form action={formAction}>
<input type="text" name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '更新'}
</button>
{state.error && (
<div className="text-red-500">{state.error}</div>
)}
{state.success && (
<div className="text-green-500">更新成功!</div>
)}
</form>
);
}
使用 useFormStatus
TypeScript
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
function Form() {
async function submitAction(formData: FormData) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
return (
<form action={submitAction}>
<input type="text" name="field" />
<SubmitButton />
</form>
);
}
Actions 的优势
- 自动管理 pending 状态
- 自动处理错误
- 支持渐进式增强(无 JS 也能工作)
- 简化的代码,无需手动 useState
复杂表单场景
动态字段
TypeScript
function DynamicFieldsForm() {
const [fields, setFields] = useState([
{ id: 1, value: '' }
]);
const addField = () => {
setFields([...fields, {
id: Date.now(),
value: ''
}]);
};
const removeField = (id: number) => {
setFields(fields.filter(field => field.id !== id));
};
const updateField = (id: number, value: string) => {
setFields(fields.map(field =>
field.id === id ? { ...field, value } : field
));
};
return (
<form>
{fields.map((field, index) => (
<div key={field.id}>
<input
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
placeholder={`字段 ${index + 1}`}
/>
{fields.length > 1 && (
<button
type="button"
onClick={() => removeField(field.id)}
>
删除
</button>
)}
</div>
))}
<button
type="button"
onClick={addField}
>
添加字段
</button>
</form>
);
}
嵌套数据结构
TypeScript
interface Address {
street: string;
city: string;
zipCode: string;
}
interface User {
name: string;
email: string;
address: Address;
}
function NestedForm() {
const [user, setUser] = useState<User>({
name: '',
email: '',
address: {
street: '',
city: '',
zipCode: ''
}
});
const updateAddress = (field: keyof Address, value: string) => {
setUser(prev => ({
...prev,
address: {
...prev.address,
[field]: value
}
}));
};
return (
<form>
<input
value={user.name}
onChange={(e) => setUser({ ...user, name: e.target.value })}
placeholder="姓名"
/>
<input
value={user.address.street}
onChange={(e) => updateAddress('street', e.target.value)}
placeholder="街道"
/>
<input
value={user.address.city}
onChange={(e) => updateAddress('city', e.target.value)}
placeholder="城市"
/>
<input
value={user.address.zipCode}
onChange={(e) => updateAddress('zipCode', e.target.value)}
placeholder="邮编"
/>
</form>
);
}
表单性能优化
防抖输入
TypeScript
import { useState, useEffect } from 'react';
function useDebounce(value: string, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function SearchForm() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// 执行搜索
performSearch(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
);
}
使用 useCallback 缓存处理函数
TypeScript
import { useState, useCallback } from 'react';
function OptimizedForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}, []);
return (
<form>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</form>
);
}
使用 useReducer 管理复杂表单
TypeScript
import { useReducer } from 'react';
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'CLEAR_ERRORS' }
| { type: 'RESET' };
interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
touched: Record<string, boolean>;
}
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: {
...state.values,
[action.field]: action.value
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'CLEAR_ERRORS':
return {
...state,
errors: {}
};
case 'RESET':
return {
values: {},
errors: {},
touched: {}
};
default:
return state;
}
}
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, {
values: {},
errors: {},
touched: {}
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
dispatch({ type: 'SET_FIELD', field: name, value });
};
return (
<form>
<input
name="username"
value={state.values.username || ''}
onChange={handleChange}
/>
{state.errors.username && (
<span className="text-red-500">{state.errors.username}</span>
)}
</form>
);
}
表单最佳实践
-
使用语义化 HTML
- 正确使用 label、input type、required 等属性,提升可访问性。
-
提供清晰的错误信息
- 错误信息应该具体、可操作,并出现在相关字段附近。
-
优化提交体验
- 禁用提交按钮、显示加载状态、防止重复提交。
-
保存草稿
- 对于长表单,定期保存草稿,避免用户数据丢失。
-
移动端优化
- 使用正确的 input type,启用自动完成,优化键盘类型。
相关资源
这篇文章有帮助吗?
Previous / Next
Related Links