React × Vue

antd、React Hook Form 与 Zod

使用 antd 构建表单界面、React Hook Form 管理状态,并通过 Zod 统一运行时校验和 TypeScript 类型。

职责分层

antd、React Hook Form 和 Zod 不应重复做同一件事:antd 负责控件与反馈,RHF 保存字段状态并组织提交,Zod 是校验与类型的唯一事实来源。

组件关系与数据流

Form State / DataUI / SubmitAdapter / ValidationSchema

协作焦点Controller 负责适配 antd 受控组件;Checkbox 需要把 event.target.checked 转换成布尔值。

实线表示值、状态或校验结果的传递,虚线表示用户修正错误后的再次输入。

flowchart TB
  accTitle: antd、React Hook Form 与 Zod 表单数据流
  accDescr: antd 控件通过 Controller 把值写入 React Hook Form,失焦或提交时 zodResolver 调用 Zod schema。校验失败时错误返回 Form.Item,成功时生成有类型保障的数据并交给提交函数。
  controls["antd 表单控件"] --> controller{{"Controller 适配值与事件"}}
  controller --> state(["React Hook Form 状态"])
  state -->|"onBlur / submit"| resolver{{"zodResolver"}}
  resolver --> schema[("Zod schema")]
  schema --> result{{"校验结果"}}
  result -->|"失败"| errors(["fieldState.error"])
  errors --> feedback["Form.Item 错误反馈"]
  feedback -.->|"用户修正"| controls
  result -->|"通过"| values(["ProfileFormValues"])
  values --> submit["submit(values) / API"]
  class state,errors,values state
  class controls,feedback,submit component
  class controller,resolver,result process
  class schema resource
  classDef state fill:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
  classDef component fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
  classDef process fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
  classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4px

RHF 维护字段状态,Zod 返回校验结果;只有通过校验的数据才会进入提交函数。

React 流程图
React 组件关系与数据流

滚轮缩放 · 按住拖动 · 双击重置 · Esc 关闭

完整 Demo 源码

以下代码直接读取在线 Demo 及其本地依赖,没有省略或改写。

React 源码

import { useState } from "react";import type { SubmitHandler } from "react-hook-form";import { Controller, useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import {  Alert,  Button,  Checkbox,  Form,  Input,  Select,} from "antd";import { SiteAntdProvider } from "@/components/SiteAntdProvider";import { profileSchema } from "./formSchema";import type { ProfileFormValues } from "./formSchema";import "./FormDemo.css";const defaultValues: ProfileFormValues = {  name: "",  email: "",  direction: "react",  agreement: false,};const directionOptions = [  { label: "React", value: "react" },  { label: "Vue", value: "vue" },  { label: "两者对比", value: "both" },];function wait(duration: number) {  return new Promise<void>((resolve) => {    setTimeout(resolve, duration);  });}export function FormDemo() {  const [submittedData, setSubmittedData] =    useState<ProfileFormValues | null>(null);  const {    control,    handleSubmit,    reset,    formState: { isSubmitting },  } = useForm<ProfileFormValues>({    resolver: zodResolver(profileSchema),    defaultValues,    mode: "onBlur",  });  const submit: SubmitHandler<ProfileFormValues> = async (values) => {    await wait(500);    setSubmittedData(values);  };  function resetForm() {    reset(defaultValues);    setSubmittedData(null);  }  return (    <SiteAntdProvider>      <div className="antd-form-demo not-content">        <div className="antd-form-demo__panel">          <form onSubmit={handleSubmit(submit)} noValidate>            <Form component={false} layout="vertical">              <Controller                name="name"                control={control}                render={({ field, fieldState }) => (                  <Form.Item                    label="姓名"                    htmlFor="profile-name"                    validateStatus={fieldState.error ? "error" : undefined}                    help={fieldState.error?.message}                  >                    <Input                      {...field}                      id="profile-name"                      placeholder="例如:小明"                      autoComplete="name"                    />                  </Form.Item>                )}              />              <Controller                name="email"                control={control}                render={({ field, fieldState }) => (                  <Form.Item                    label="邮箱"                    htmlFor="profile-email"                    validateStatus={fieldState.error ? "error" : undefined}                    help={fieldState.error?.message}                  >                    <Input                      {...field}                      id="profile-email"                      type="email"                      placeholder="name@example.com"                      autoComplete="email"                    />                  </Form.Item>                )}              />              <Controller                name="direction"                control={control}                render={({ field, fieldState }) => (                  <Form.Item                    label="学习方向"                    htmlFor="profile-direction"                    validateStatus={fieldState.error ? "error" : undefined}                    help={fieldState.error?.message}                  >                    <Select                      {...field}                      id="profile-direction"                      options={directionOptions}                    />                  </Form.Item>                )}              />              <Controller                name="agreement"                control={control}                render={({ field, fieldState }) => (                  <Form.Item                    className="antd-form-demo__agreement"                    validateStatus={fieldState.error ? "error" : undefined}                    help={fieldState.error?.message}                  >                    <Checkbox                      name={field.name}                      ref={field.ref}                      checked={field.value}                      onBlur={field.onBlur}                      onChange={(event) => field.onChange(event.target.checked)}                    >                      同意保存这份学习资料                    </Checkbox>                  </Form.Item>                )}              />              <div className="antd-form-demo__actions">                <Button                  type="primary"                  htmlType="submit"                  loading={isSubmitting}                >                  提交资料                </Button>                <Button htmlType="button" onClick={resetForm}>                  重置                </Button>              </div>            </Form>          </form>        </div>        <div className="antd-form-demo__result" aria-live="polite">          {submittedData ? (            <Alert              type="success"              showIcon              title="校验通过,已获得类型安全的数据"              description={                <pre>{JSON.stringify(submittedData, null, 2)}</pre>              }            />          ) : (            <Alert              type="info"              showIcon              title="提交后会在这里显示通过 Zod 校验的数据"            />          )}        </div>      </div>    </SiteAntdProvider>  );}

在线 Demo

填写并提交表单,观察必填、格式和勾选校验。校验通过后会展示提交函数实际收到的数据。

React

组合原则

  • 使用 zodResolver 接入 schema,避免手动同步两套校验结果。
  • antd 受控组件通过 Controller 连接 RHF;Checkbox 需要把 event.target.checked 转为布尔值。
  • Form.Item 只负责布局与错误展示,不再配置重复的 rules。
  • 通过 z.infer 从 schema 推导表单类型,让运行时规则与 TypeScript 结构保持一致。

何时不需要这套组合

字段很少、没有复杂校验的表单可以直接使用受控状态。库的价值来自字段规模、性能和校验一致性,而不是表单出现就必须引入。

先复习受控表单与 v-model,再决定是否需要专门的表单状态库。