关于 react-forms
面向 React 开发者的表单模式技能,涵盖受控与非受控输入、React Hook Form 和验证实践,并提供常见问题排查建议。
主要功能
- 01受控与非受控表单
- 02React Hook Form 用法
- 03表单数组与受控组件
- 04验证和可访问性实践
- 05提交问题排查
适用场景
- 01选择适合需求的输入管理方式
- 02为表单设置模式验证
- 03处理动态表单字段
- 04排查提交或验证故障
MCP 相关
提及通过 mcp__documentation__fetch_docs 获取 React 表单文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-forms安装后在 Claude Code 中输入/react-forms确认已加载。
附带文件advanced.md
详细介绍
React 表单
深入了解:使用
mcp__documentation__fetch_docs,指定 technology:react、topic:forms,获取有关 React 表单模式和验证策略的完整文档。
完整参考:参阅 advanced.md,了解 Server Actions、多步表单、依赖字段、自动保存、验证模式和无障碍功能。
受控与非受控
受控表单
由 React 状态控制输入:
function ControlledForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: Record<string, string> = {};
if (!formData.email) newErrors.email = 'Email required';
if (!formData.password) newErrors.password = 'Password required';
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log('Submitting:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
非受控表单(useRef)
由 DOM 控制输入:
function UncontrolledForm() {
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const data = {
email: emailRef.current?.value,
password: passwordRef.current?.value,
};
console.log('Submitting:', data);
};
return (
<form onSubmit={handleSubmit}>
<input ref={emailRef} name="email" type="email" />
<input ref={passwordRef} name="password" type="password" />
<button type="submit">Submit</button>
</form>
);
}
FormData API(非受控)
function FormDataForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
};
console.log('Submitting:', data);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Submit</button>
</form>
);
}
React Hook Form
最受欢迎的表单库——采用非受控方式,开发体验出色:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});
type FormData = z.infer<typeof schema>;
function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', confirmPassword: '' },
});
const onSubmit = async (data: FormData) => {
try {
await signUp(data);
reset();
} catch (error) {
console.error('Sign up failed:', error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<input {...register('password')} type="password" placeholder="Password" />
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<div>
<input {...register('confirmPassword')} type="password" placeholder="Confirm Password" />
{errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing up...' : 'Sign Up'}
</button>
</form>
);
}
表单数组
import { useFieldArray, useForm } from 'react-hook-form';
interface OrderForm {
items: { productId: string; quantity: number }[];
notes: string;
}
function OrderForm() {
const { register, control, handleSubmit } = useForm<OrderForm>({
defaultValues: {
items: [{ productId: '', quantity: 1 }],
notes: '',
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<select {...register(`items.${index}.productId`)}>
<option value="">Select product</option>
{products.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
type="number"
min="1"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
Add Item
</button>
<textarea {...register('notes')} placeholder="Notes" />
<button type="submit">Submit Order</button>
</form>
);
}
使用 RHF 的受控组件
import { Controller, useForm } from 'react-hook-form';
import { DatePicker } from '@/components/date-picker';
function EventForm() {
const { control, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="date"
control={control}
rules={{ required: 'Date is required' }}
render={({ field, fieldState }) => (
<div>
<DatePicker
value={field.value}
onChange={field.onChange}
/>
{fieldState.error && (
<span className="error">{fieldState.error.message}</span>
)}
</div>
)}
/>
<button type="submit">Create Event</button>
</form>
);
}
最佳实践
| 应做 | 不应做 |
|---|---|
| 复杂表单使用 React Hook Form | 手动管理所有表单状态 |
| 使用 Zod/Yup 进行验证 | 从头编写验证逻辑 |
| 显示行内错误 | 一次性显示所有错误 |
使用 aria-* 属性 | 忽略无障碍功能 |
| 对开销大的验证进行防抖 | 每次按键都执行异步验证 |
- ✅ 需要实时验证时使用受控输入
- ✅ 使用非受控输入(RHF)以获得更好的性能
- ✅ 在失焦时验证,改善用户体验
- ✅ 提交期间显示加载状态
- ❌ 不要在表单为空时禁用提交按钮
- ❌ 出错时不要清空表单
- ❌ 用户交互前不要显示错误
何时不应使用此技能
- 使用 Zod 模式的 React Hook Form - 使用
react-hook-form技能了解详细模式 - React 19 Server Actions 表单 - 使用
react-19技能了解 useActionState 和 useFormStatus - 简单的输入处理 - 使用
react技能了解基本的受控/非受控输入 - RHF 之外的表单库 - 遵循相应库的专属文档
反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 手动管理所有表单状态 | 样板代码多、容易出错 | 使用 React Hook Form |
| 不验证用户输入 | 安全性/用户体验问题 | 使用 Zod 验证模式 |
| 挂载时显示所有错误 | 用户体验差 | 在失焦或提交时验证 |
| 表单为空时禁用提交 | 阻止显示验证消息 | 允许提交并显示错误 |
| 验证错误时清空表单 | 用户会丢失数据 | 保留表单数据并显示错误 |
| 不显示加载状态 | 用户体验令人困惑 | 提交期间显示加载状态 |
| 缺少无障碍属性 | 无法访问 | 添加 aria 标签和角色 |
| 每次按键都执行异步验证 | 性能问题 | 对异步验证进行防抖 |
快速故障排除
| 问题 | 可能原因 | 修复方法 |
|---|---|---|
| 表单未提交 | 缺少 onSubmit 或 action | 为表单添加 onSubmit 处理函数 |
| 验证不起作用 | 未应用模式 | 检查 useForm 中的 resolver |
| 输入未更新 | 未正确受控 | 确保已设置 value 和 onChange |
| 未显示错误 | 缺少 FormMessage | 添加 FormMessage 组件 |
| 提交按钮始终处于禁用状态 | isPending 检查错误 | 使用 formState.isSubmitting |
| 值未重置 | 未调用 reset() | 成功后调用 form.reset() |
| 异步验证过于频繁 | 未使用防抖 | 对验证函数进行防抖 |
参考文档
- React Hook Form
- Zod
- MCP:
mcp__documentation__fetch_docs→ technology:react,topic:forms
评论
支持 Markdown