表单 (react-hook-form + zod)
规则
- 模式优先:定义
zod 模式,通过 z.infer 派生类型。客户端和服务端共享唯一的真实数据源。
- 使用
@hookform/resolvers/zod(zodResolver)。模式设置为 onTouched 以优化用户体验;在服务端操作中再次使用相同模式进行校验。
- 为表单指定类型:
useForm<FormValues>。不允许出现 any 类型的字段值。
- 无障碍错误提示:为错误消息添加
aria-invalid、aria-describedby、role="alert" 属性。使用 <label htmlFor> 关联标签。
- 通过 Server Action(
formAction 或调用服务端函数的 action)提交。显示待提交状态和服务器错误状态。
模式
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const Schema = z.object({
email: z.string().email('请输入有效的邮箱地址'),
password: z.string().min(8, '至少 8 个字符'),
});
type Values = z.infer<typeof Schema>;
export function LoginForm({ onSubmit }: { onSubmit: (v: Values) => Promise<{ ok: boolean; error?: string }> }) {
const { register, handleSubmit, formState: { errors, isSubmitting }, setError } = useForm<Values>({
resolver: zodResolver(Schema),
mode: 'onTouched',
});
return (
<form onSubmit={handleSubmit(async (v) => {
const r = await onSubmit(v);
if (!r.ok) setError('root', { message: r.error ?? '操作失败' });
})}>
<label htmlFor="email">邮箱</label>
<input id="email" type="email" autoComplete="email"
aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-err' : undefined}
{...register('email')} />
{errors.email && <p id="email-err" role="alert">{errors.email.message}</p>}
<label htmlFor="password">密码</label>
<input id="password" type="password" autoComplete="current-password" {...register('password')} />
{errors.password && <p role="alert">{errors.password.message}</p>}
{errors.root && <p role="alert">{errors.root.message}</p>}
<button disabled={isSubmitting}>{isSubmitting ? '正在登录…' : '登录'}</button>
</form>
);
}
检查清单