ReactHookForm+Zod验证 react-hook-form-zod

此技能用于在 React 应用中实现类型安全的表单验证,结合 React Hook Form 和 Zod 库。它支持表单模式定义、字段数组、多步骤表单、客户端和服务器端验证,并解决常见错误如未控制到控制警告。关键词:React Hook Form, Zod, 表单验证, 类型安全, 前端开发, React 表单, 性能优化, 可访问性。

前端开发 0 次安装 0 次浏览 更新于 3/7/2026

name: react-hook-form-zod description: 使用 React Hook Form 和 Zod 验证的类型安全 React 表单。用于表单模式、字段数组、多步骤表单,或遇到验证错误、解析器问题、嵌套字段问题。

关键词:react-hook-form, useForm, zod 验证, zodResolver, @hookform/resolvers, 表单模式, register, handleSubmit, formState, useFieldArray, useWatch, useController, Controller, shadcn 表单, Field 组件, 客户端服务器验证, 嵌套验证, 数组字段验证, 动态字段, 多步骤表单, 异步验证, zod refine, z.infer, 表单错误处理, 未控制到控制, 解析器未找到, 模式验证错误

license: MIT

React Hook Form + Zod 验证

状态: 生产就绪 ✅ 最后更新: 2025-11-21 依赖项: 无(独立) 最新版本: react-hook-form@7.66.1, zod@4.1.12, @hookform/resolvers@5.2.2


快速开始(10分钟)

1. 安装包

bun add react-hook-form@7.66.1 zod@4.1.12 @hookform/resolvers@5.2.2

为什么选择这些包:

  • react-hook-form: 高性能、灵活的表单,最少重渲染
  • zod: TypeScript 优先的模式验证,支持类型推断
  • @hookform/resolvers: 连接 Zod 到 React Hook Form 的适配器

2. 创建你的第一个表单

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

// 1. 定义验证模式
const loginSchema = z.object({
  email: z.string().email('无效的邮箱地址'),
  password: z.string().min(8, '密码必须至少8个字符'),
})

// 2. 从模式推断 TypeScript 类型
type LoginFormData = z.infer<typeof loginSchema>

function LoginForm() {
  // 3. 使用 zodResolver 初始化表单
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  // 4. 处理表单提交
  const onSubmit = async (data: LoginFormData) => {
    // 数据在这里保证有效
    console.log('有效数据:', data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">邮箱</label>
        <input id="email" type="email" {...register('email')} />
        {errors.email && (
          <span role="alert" className="error">
            {errors.email.message}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">密码</label>
        <input id="password" type="password" {...register('password')} />
        {errors.password && (
          <span role="alert" className="error">
            {errors.password.message}
          </span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '登录中...' : '登录'}
      </button>
    </form>
  )
}

关键点:

  • 始终设置 defaultValues 以防止“未控制到控制”警告
  • 使用 zodResolver(schema) 连接 Zod 验证
  • 使用 z.infer<typeof schema> 进行类型安全
  • 同时在客户端和服务器端验证(绝不只信任客户端验证)

模板: 参见 templates/basic-form.tsx 获取完整工作示例

3. 添加服务器端验证

// server/api/login.ts
import { z } from 'zod'

// 服务器上使用相同的模式
const loginSchema = z.object({
  email: z.string().email('无效的邮箱地址'),
  password: z.string().min(8, '密码必须至少8个字符'),
})

export async function loginHandler(req: Request) {
  try {
    const data = loginSchema.parse(await req.json())
    // 数据是类型安全和已验证的
    return { success: true }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: error.flatten().fieldErrors }
    }
    throw error
  }
}

为什么需要服务器验证:

  • 客户端验证可以被绕过(检查元素、Postman、curl)
  • 服务器验证是你的安全层
  • 相同的 Zod 模式 = 单一来源

模板: 参见 templates/server-validation.ts


核心概念

useForm 钩子

const {
  register,           // 注册输入字段
  handleSubmit,       // 包装 onSubmit 处理器
  formState,          // 表单状态(错误、isValid、isDirty 等)
  setValue,           // 以编程方式设置字段值
  getValues,          // 获取当前表单值
  watch,              // 监视字段值
  reset,              // 重置表单为默认值
  trigger,            // 手动触发验证
  control,            // 用于 Controller/useController
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onSubmit',               // 何时验证
  defaultValues: {},              // 初始值(必须)
})

验证模式:

  • onSubmit - 在提交时验证(最佳性能)
  • onChange - 在每次更改时验证(实时反馈)
  • onBlur - 在字段失去焦点时验证(良好平衡)
  • all - 在提交、失去焦点和更改时验证

参考: 参见 references/rhf-api-reference.md 获取完整 API

Zod 模式基础

import { z } from 'zod'

// 基本类型
const schema = z.object({
  email: z.string().email('无效的邮箱'),
  age: z.number().min(18, '必须18岁以上'),
  terms: z.boolean().refine(val => val === true, '必须接受条款'),
})

// 嵌套对象
const addressSchema = z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email(),
  }),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string().regex(/^\d{5}$/),
  }),
})

// 数组
const tagsSchema = z.object({
  tags: z.array(z.string()).min(1, '至少需要一个标签'),
})

// 可选和可空
const optionalSchema = z.object({
  middleName: z.string().optional(),
  nickname: z.string().nullable(),
  bio: z.string().nullish(), // 可选且可空
})

参考: 参见 references/zod-schemas-guide.md 获取完整模式


关键规则

始终要做

始终设置 defaultValues - 防止“未控制到控制”警告 ✅ 使用 zodResolver 进行验证 - 连接 Zod 模式到 React Hook Form ✅ 从模式推断类型 - 使用 z.infer<typeof schema> 确保类型安全 ✅ 同时在服务器端验证 - 客户端验证可以被绕过 ✅ 使用 .register() 用于原生输入 - 简单且性能高 ✅ 使用 Controller 用于自定义组件 - 适用于组件库(MUI、Chakra 等) ✅ 处理可访问性错误 - 使用 role="alert" 用于屏幕阅读器 ✅ 提交后重置表单 - 使用 reset() 清除表单状态

表单模式: 参见 templates/ 获取:

  • basic-form.tsx - 简单的登录/注册表单
  • advanced-form.tsx - 嵌套对象、数组、动态字段
  • shadcn-form.tsx - 与 shadcn/ui 集成
  • multi-step-form.tsx - 向导/步骤表单
  • async-validation.tsx - 异步字段验证

永远不要做

永远不要跳过 defaultValues - 导致“未控制到控制”错误 ❌ 永远不要只使用客户端验证 - 安全漏洞 ❌ 永远不要直接改变表单值 - 使用 setValue() 代替 ❌ 永远不要忽略可访问性 - 始终使用适当的标签和 ARIA ❌ 永远不要忘记在 isSubmitting 时禁用提交 - 防止双重提交

性能: 参见 references/performance-optimization.md 获取:

  • 何时使用 mode: 'onBlur' 对比 'onChange'
  • useWatch 对比 watch()
  • 重渲染优化策略

可访问性: 参见 references/accessibility.md 获取:

  • 正确的标签关联
  • 错误公告
  • 焦点管理
  • 键盘导航

前5大关键错误

错误 #1: 未控制到控制警告 ⚠️

错误:

警告: 一个组件正在将一个未控制的输入更改为受控

原因: 未设置 defaultValues

解决方案:

// ❌ 错误
const form = useForm()

// ✅ 正确
const form = useForm({
  defaultValues: {
    email: '',
    password: '',
  }
})

错误 #2: Zod v4 类型推断问题

错误: 类型推断不正确

解决方案:

// 如果需要,显式类型化 useForm
const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})

来源: GitHub Issue #13109


错误 #3: 解析器未找到

错误:

模块未找到: 无法解析 '@hookform/resolvers/zod'

解决方案:

# 安装解析器包
bun add @hookform/resolvers@5.2.2

错误 #4: 数组字段问题

错误: 动态数组字段与 useFieldArray 不工作

解决方案:

const { fields, append, remove } = useFieldArray({
  control,
  name: "items" // 必须完全匹配模式字段名
})

模板: 参见 templates/dynamic-fields.tsx


错误 #5: 自定义组件验证失败

错误: 第三方组件(MUI、Chakra)不验证

解决方案: 使用 Controller 代替 register:

<Controller
  name="date"
  control={control}
  render={({ field }) => (
    <DatePicker {...field} />
  )}
/>

参考: 参见 references/error-handling.md 获取所有模式


所有12个错误: 参见 references/top-errors.md 获取完整文档


常见模式

基本表单

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

const schema = z.object({
  name: z.string().min(1, '需要姓名'),
  email: z.string().email('无效的邮箱'),
})

type FormData = z.infer<typeof schema>

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: { name: '', email: '' }
  })

  const onSubmit = (data: FormData) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      <button type="submit">提交</button>
    </form>
  )
}

模板: 参见 templates/basic-form.tsx


动态字段 (useFieldArray)

import { useForm, useFieldArray } from 'react-hook-form'

const schema = z.object({
  items: z.array(
    z.object({
      name: z.string(),
      quantity: z.number().min(1)
    })
  ).min(1, '至少需要一个项目')
})

function DynamicForm() {
  const { control, handleSubmit } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { items: [{ name: '', quantity: 1 }] }
  })

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

  return (
    <form>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`items.${index}.name`)} />
          <button onClick={() => remove(index)}>移除</button>
        </div>
      ))}
      <button onClick={() => append({ name: '', quantity: 1 })}>
        添加项目
      </button>
    </form>
  )
}

模板: 参见 templates/dynamic-fields.tsx


异步验证

const schema = z.object({
  username: z.string()
    .min(3)
    .refine(async (username) => {
      const response = await fetch(`/api/check-username?username=${username}`)
      const { available } = await response.json()
      return available
    }, '用户名已存在')
})

模板: 参见 templates/async-validation.tsx


多步骤表单

function MultiStepForm() {
  const [step, setStep] = useState(1)
  const form = useForm({
    resolver: zodResolver(schema),
    mode: 'onBlur' // 在继续前验证每一步
  })

  const onSubmit = async (data) => {
    if (step < 3) {
      setStep(step + 1)
    } else {
      // 最终提交
      await submitForm(data)
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {step === 1 && <Step1Fields />}
      {step === 2 && <Step2Fields />}
      {step === 3 && <Step3Fields />}
      <button type="submit">
        {step < 3 ? '下一步' : '提交'}
      </button>
    </form>
  )
}

模板: 参见 templates/multi-step-form.tsx


shadcn/ui 集成

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'

function ShadcnForm() {
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { email: '' }
  })

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>邮箱</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
    </Form>
  )
}

参考: 参见 references/shadcn-integration.md 获取完整模式 模板: 参见 templates/shadcn-form.tsx


使用捆绑资源

模板 (templates/)

复制粘贴就绪的示例:

  • basic-form.tsx - 简单的登录/注册表单带验证
  • advanced-form.tsx - 嵌套对象、数组、条件字段
  • shadcn-form.tsx - shadcn/ui Form 组件集成
  • multi-step-form.tsx - 向导/步骤表单带步骤验证
  • dynamic-fields.tsx - 用于动态表单字段的 useFieldArray
  • async-validation.tsx - 异步字段验证(用户名检查等)
  • server-validation.ts - 服务器端验证带 Zod
  • custom-error-display.tsx - 自定义错误消息组件
  • package.json - 包版本和脚本

参考 (references/)

详细文档:


何时加载参考

参考 何时加载…
top-errors.md 调试验证问题、类型错误或“未控制到控制”警告
rhf-api-reference.md 需要完整的 API 用于 useForm、register、Controller、formState
zod-schemas-guide.md 构建复杂模式(嵌套、数组、条件、异步验证)
shadcn-integration.md 使用 shadcn/ui Form、FormField、FormItem 组件
error-handling.md 自定义错误显示、验证时机、错误消息模式
performance-optimization.md 表单重渲染太多,优化 watch/useWatch
accessibility.md WCAG 合规、屏幕阅读器、键盘导航
links-to-official-docs.md 需要官方文档链接

性能提示

快速提示:

  • 使用 mode: 'onBlur' 以平衡用户体验和性能
  • 使用 useWatch 代替 watch() 用于特定字段
  • 在组件外记忆验证模式
  • 使用 shouldUnregister: false 用于条件字段
  • 避免 watch() 不带参数(监视所有字段)

参考: 参见 references/performance-optimization.md 获取完整策略


可访问性

快速检查清单:

  • ✅ 使用 <label htmlFor="fieldId"> 用于所有输入
  • ✅ 添加 role="alert" 到错误消息
  • ✅ 使用 aria-invalid="true" 在无效字段上
  • ✅ 确保键盘导航工作(Tab、Enter、Escape)
  • ✅ 提供清晰、可操作的错误消息

参考: 参见 references/accessibility.md 获取 WCAG 合规指南


验证模式 (Zod)

常见模式:

// 邮箱
z.string().email('无效的邮箱')

// 密码(最少8字符,1大写,1数字)
z.string()
  .min(8)
  .regex(/[A-Z]/, '需要大写字母')
  .regex(/[0-9]/, '需要数字')

// URL
z.string().url('无效的 URL')

// 日期
z.string().datetime() // ISO 8601
z.date() // JS Date 对象

// 文件上传
z.instanceof(File)
  .refine(file => file.size <= 5000000, '最大5MB')
  .refine(
    file => ['image/jpeg', 'image/png'].includes(file.type),
    '只允许 JPEG/PNG'
  )

// 自定义验证
z.string().refine(
  val => val !== 'admin',
  '用户名“admin”已保留'
)

// 异步验证
z.string().refine(
  async (username) => {
    const available = await checkUsername(username)
    return available
  },
  '用户名已存在'
)

参考: 参见 references/zod-schemas-guide.md 获取所有模式


依赖项

必需:

  • react-hook-form@7.65.0 - 表单状态管理
  • zod@4.1.12 - 模式验证
  • @hookform/resolvers@5.2.2 - 验证适配器

可选:

  • @radix-ui/react-label@latest - 用于 shadcn/ui 集成
  • class-variance-authority@latest - 用于 shadcn/ui 样式

官方文档

参考: 参见 references/links-to-official-docs.md 获取组织的链接


故障排除

“未控制到控制”警告

解决方案: 始终设置 defaultValues → 参见 references/top-errors.md #2

Zod v4 类型推断问题

解决方案: 显式类型化 useForm<z.infer<typeof schema>> → 参见 references/top-errors.md #1

解析器未找到错误

解决方案: 安装 @hookform/resolvers 包 → 参见 references/top-errors.md #3

自定义组件不验证

解决方案: 使用 Controller 代替 register → 参见 references/top-errors.md #5

表单重渲染太多

解决方案: 使用 mode: 'onBlur'useWatch → 参见 references/performance-optimization.md


生产示例

此技能基于生产模式:

  • 真实世界表单: 登录、注册、结账、多步骤向导
  • 验证: 客户端 + 服务器端共享 Zod 模式
  • 可访问性: WCAG 2.1 AA 合规
  • 性能: 优化以减少重渲染

令牌节省: ~60%(全面的表单模式带模板) 错误预防: 100%(所有12个文档错误带解决方案) 生产就绪!