📚 现代前端开发 目录
阶段3:React
第 19 / 35 课

表单与验证

构建健壮的表单系统

📖 核心概念

💻 代码实现

React Hook Form + Zod ✅ tsx
import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Zod Schema定义
const userSchema = z.object({
  name: z.string().min(2, '姓名至少2个字符').max(20, '姓名最多20个字符'),
  email: z.string().email('请输入有效的邮箱地址'),
  password: z.string()
    .min(8, '密码至少8位')
    .regex(/[A-Z]/, '密码需包含大写字母')
    .regex(/[0-9]/, '密码需包含数字'),
  confirmPassword: z.string(),
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
  address: z.object({
    province: z.string().min(1, '请选择省份'),
    city: z.string().min(1, '请选择城市'),
    detail: z.string().min(5, '详细地址至少5个字符'),
  }),
  skills: z.array(z.object({
    name: z.string().min(1, '技能名称不能为空'),
    level: z.enum(['beginner', 'intermediate', 'advanced']),
  })).min(1, '至少添加一项技能'),
  agree: z.literal(true, { message: '请同意用户协议' }),
}).refine(data => data.password === data.confirmPassword, {
  message: '两次密码输入不一致',
  path: ['confirmPassword'],
});

type UserFormData = z.infer<typeof userSchema>;

function RegistrationForm() {
  const {
    register, handleSubmit, control, formState: { errors, isSubmitting },
    watch, reset, setError,
  } = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
    defaultValues: {
      skills: [{ name: '', level: 'beginner' }],
    },
  });

  const { fields, append, remove } = useFieldArray({ control, name: 'skills' });

  const onSubmit = async (data: UserFormData) => {
    try {
      await api.register(data);
      toast.success('注册成功!');
      reset();
    } catch (err) {
      setError('root', { message: '注册失败,请重试' });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="form">
      <FormField label="姓名" error={errors.name}>
        <input {...register('name')} placeholder="请输入姓名" />
      </FormField>

      <FormField label="邮箱" error={errors.email}>
        <input {...register('email')} type="email" placeholder="your@email.com" />
      </FormField>

      <FormField label="密码" error={errors.password}>
        <input {...register('password')} type="password" />
      </FormField>

      <FormField label="确认密码" error={errors.confirmPassword}>
        <input {...register('confirmPassword')} type="password" />
      </FormField>

      {/* 嵌套对象 */}
      <FormField label="地址">
        <Controller name="address.province" control={control}
          render={({ field }) => <ProvinceSelect {...field} />}
        />
        <Controller name="address.city" control={control}
          render={({ field }) => <CitySelect {...field} province={watch('address.province')} />}
        />
        <input {...register('address.detail')} placeholder="详细地址" />
      </FormField>

      {/* 动态字段数组 */}
      <div className="skills-section">
        <h3>技能</h3>
        {fields.map((field, index) => (
          <div key={field.id} className="skill-row">
            <input {...register(\`skills.\${index}.name\`)} placeholder="技能名称" />
            <select {...register(\`skills.\${index}.level\`)}>
              <option value="beginner">初级</option>
              <option value="intermediate">中级</option>
              <option value="advanced">高级</option>
            </select>
            <button type="button" onClick={() => remove(index)}>删除</button>
          </div>
        ))}
        <button type="button" onClick={() => append({ name: '', level: 'beginner' })}>
          + 添加技能
        </button>
        {errors.skills && <p className="error">{errors.skills.message}</p>}
      </div>

      <label className="checkbox">
        <input type="checkbox" {...register('agree')} />
        我同意用户协议
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '注册'}
      </button>
    </form>
  );
}

🎯 练习任务

🏆 成就解锁
表单工程师 — 掌握React表单开发与验证,构建用户友好的数据输入体验