返回首页

React Hook Form 和 Zod:一年的运营经验

在生产环境中使用 React Hook Form 和 Zod 一年经验。分析减少样板代码的优势并识别隐藏问题,如处理 undefined、类型错误和 isDirty 标志。为避免典型错误的推荐。

React Hook Form 和 Zod:生产环境中的真实案例
Advertisement 728x90

React Hook Form 与 Zod:在生产环境中使用一年,无样板代码或隐藏陷阱

在大型项目生产环境中积极使用 React Hook Form 和 Zod 组合一年后,团队确认:声明式表单方法可以将代码量减少 30–40%,但需要注意实现细节。我们分享了发现的陷阱以及如何克服它们。

从 MobX 到声明式表单:为什么切换方法?

在之前的项目迭代中,表单使用 MobX 以 MVC 风格构建。这种方法允许控制状态,但会产生样板代码:每个字段都需要在控制器中单独的方法、错误数组,以及与 UI 的手动同步。例如,添加新字段意味着创建:

export class SomeController {
  public setDuration = (v: number) => {
    this.store.duration = v;
  }
}

这样的模板代码与业务逻辑无关,却占用了组件体积的 60%。切换到 React Hook Form (RHF) 与 Zod 解决了问题:验证 schema 以声明式描述,库自动处理状态同步和错误处理。

Google AdInline article slot

基本表单示例:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const formSchema = z.object({
  name: z.string().min(1, 'Enter name'),
  email: z.string().email('Invalid email'),
  age: z.coerce.number().min(0, 'Invalid vozrast').optional(),
});

type FormValues = z.infer<typeof formSchema>;

export function ExampleForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
      email: '',
    },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <div>
        <input {...register('name')} />
        {errors.name && <span>{errors.name.message}</span>}
      </div>
      <div>
        <input type="email" {...register('email')} />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      <div>
        <input type="number" {...register('age')} />
        {errors.age && <span>{errors.age.message}</span>}
      </div>
      <button type="submit">Send</button>
    </form>
  );
}

结果:代码量大幅缩小,维护变得更简单,开发新表单的速度显著加快。然而,在实际使用中,文档中未提及的细微问题逐渐浮出水面。

React Hook Form 与 Zod 组合的三大陷阱

使用 RHF 和 Zod 一年,我们遇到了三个关键问题,这些问题在特定场景下会破坏表单功能。

Google AdInline article slot

1. 处理 undefined 和 null

RHF 不支持 undefined 作为字段值。尝试设置:

methods.setValue('age', undefined)

会导致字段忽略该更改。文档中提到了这一点,但并不明显。标准修复方法是使用 null,并将字段声明为 .nullable()

const schema = z.object({
  age: z.number().nullable()
});

methods.setValue('age', null);

然而,这可能与期望接收数字的自定义组件冲突。例如,年龄输入组件在遇到 null 时可能出错。另一方面:组件清空时返回 undefined,但 RHF 不会处理该值。

Google AdInline article slot

重置按钮会增加额外复杂性。reset() 方法从 defaultValues 获取值,但如果它们被设置为 undefined,重置就会失败。解决方案:字符串字段使用 null 或空字符串。

2. Zod 中的意外类型错误

问题出现在数据类型不匹配时。例如,这个 schema:

const schema = z.object({
  field1: z.string().min(1, 'Fill field').optional()
});

如果组件返回 null(而非 undefined),Zod 会抛出 invalid type (expected string received null) 错误。用户会看到系统消息,而不是你的自定义文本。

解决方案:为所有验证类型显式设置错误文本:

const schema = z.object({
  field1: z.string('Fill field').min(1, 'Fill field').optional()
});

这样确保任何错误都会显示你的消息。

3. isDirty 标志的不稳定性

formState.isDirty 标志用于显示表单是否有更改。但使用空 defaultValues 初始化:

const methods = useForm({
  resolver: zodResolver(formSchema),
  defaultValues: {},
});

会导致 isDirty 在渲染后立即变为 true,即使用户尚未触碰任何内容。与此同时,dirtyFields 仍为空对象。

这是由于 defaultValues 结构与 schema 不匹配所致。为了避免假阳性,确保 defaultValues 包含 schema 中的每个字段——即使使用 undefinednull

如何避免常见错误?

根据经验,这里是经过验证的实践方法:

  • 处理空值:

- 字符串字段:默认使用空字符串。

- 数字/对象字段:在 Zod 中应用 .nullable(),值使用 null

- 始终验证自定义组件是否支持所选类型。

  • 可靠验证:

- 如上例,为每个验证步骤指定通用错误消息。

- 在组件开发时测试 nullundefined 场景。

  • 正确的 isDirty 行为:

- 初始化 defaultValues 以匹配 schema,覆盖所有字段。

- 避免使用空对象 {}——显式列出字段,并使用 undefinednull

我们还推荐在项目中添加统一的表单辅助函数,例如处理 RHF 特殊情况的重置函数:

const resetForm = () => {
  methods.reset({
    name: null,
    email: null,
    age: null,
  });
};

关键要点

  • 代码减少并不消除测试需求: 声明式方法节省时间,但需要对边缘情况进行严格检查。
  • 类型安全是你的朋友: Zod 与 TypeScript 可防止许多 bug,但 schema 必须正确配置。
  • 记录怪癖: 在项目内部文档中记录常见的 RHF 和 Zod 问题的变通方法。

— Editorial Team

Advertisement 728x90

继续阅读