mcpField
R
react-hook-form-zod

react-hook-form-zod

github.com/jezweb开发1.1k1.2k 次安装3 周前更新
代码仓库

关于 react-hook-form-zod

面向 React 表单开发者的实践指南,介绍 React Hook Form 与 Zod 的组合方式。内容涵盖表单验证、错误处理、动态字段及大型表单性能注意事项。

主要功能

  1. 01React Hook Form 与 Zod 集成
  2. 02客户端与服务端验证
  3. 03字段错误处理与映射
  4. 04动态字段及多步骤表单
  5. 05大型表单性能建议

适用场景

  1. 01构建带类型推断的 React 表单
  2. 02验证跨字段或条件输入
  3. 03将服务端错误显示到表单字段
  4. 04排查大型表单性能问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill react-hook-form-zod

安装后在 Claude Code 中输入/react-hook-form-zod确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
accessibility.md
error-handling.md
links-to-official-docs.md
performance-optimization.md
rhf-api-reference.md
shadcn-integration.md
top-errors.md
zod-schemas-guide.md
rules/
react-hook-form-zod.md
scripts/
check-versions.sh
templates/
advanced-form.tsx
async-validation.tsx
basic-form.tsx
custom-error-display.tsx
dynamic-fields.tsx
multi-step-form.tsx
package.json
server-validation.ts
shadcn-form.tsx

详细介绍

React Hook Form + Zod 验证

状态:已可用于生产环境 ✅ 上次验证时间:2026-01-20 最新版本:[email protected], [email protected], @hookform/[email protected]


快速开始

npm install [email protected] [email protected] @hookform/[email protected]

基本表单模式:

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

type FormData = z.infer<typeof schema>

const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
  resolver: zodResolver(schema),
  defaultValues: { email: '', password: '' }, // REQUIRED to prevent uncontrolled warnings
})

<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register('email')} />
  {errors.email && <span role="alert">{errors.email.message}</span>}
</form>

服务端验证(至关重要 — 绝不可跳过):

// SAME schema on server
const data = schema.parse(await req.json())

关键模式

useForm 选项(验证模式):

  • mode: 'onSubmit'(默认) - 性能最佳
  • mode: 'onBlur' - 平衡性好
  • mode: 'onChange' - 实时反馈,但会触发更多次重新渲染
  • shouldUnregister: true - 卸载时移除字段数据(用于多步表单)

Zod 精炼(跨字段验证):

z.object({ password: z.string(), confirm: z.string() })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ['confirm'], // CRITICAL: Error appears on this field
  })

Zod 转换:

z.string().transform((val) => val.toLowerCase()) // Data manipulation
z.string().transform(parseInt).refine((v) => v > 0) // Chain with refine

Zod v4.3.0+ 功能:

// Exact optional (can omit field, but NOT undefined)
z.string().exactOptional()

// Exclusive union (exactly one must match)
z.xor([z.string(), z.number()])

// Import from JSON Schema
z.fromJSONSchema({ type: "object", properties: { name: { type: "string" } } })

zodResolver 将 Zod 接入 React Hook Form,同时保留类型安全


注册

register(用于标准 HTML 输入控件):

<input {...register('email')} /> // Uncontrolled, best performance

Controller(用于第三方组件):

<Controller
  name="category"
  control={control}
  render={({ field }) => <CustomSelect {...field} />} // MUST spread {...field}
/>

何时使用 Controller:React Select、日期选择器、没有 ref 的自定义组件。其他情况使用 register。


错误处理

显示错误:

{errors.email && <span role="alert">{errors.email.message}</span>}
{errors.address?.street?.message} // Nested errors (use optional chaining)

服务端错误:

const onSubmit = async (data) => {
  const res = await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
  if (!res.ok) {
    const { errors: serverErrors } = await res.json()
    Object.entries(serverErrors).forEach(([field, msg]) => setError(field, { message: msg }))
  }
}

高级模式

useFieldArray(动态列表):

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

{fields.map((field, index) => (
  <div key={field.id}> {/* CRITICAL: Use field.id, NOT index */}
    <input {...register(`contacts.${index}.name` as const)} />
    {errors.contacts?.[index]?.name && <span>{errors.contacts[index].name.message}</span>}
    <button onClick={() => remove(index)}>Remove</button>
  </div>
))}
<button onClick={() => append({ name: '', email: '' })}>Add</button>

异步验证(防抖):

const debouncedValidation = useDebouncedCallback(() => trigger('username'), 500)

多步表单:

const step1 = z.object({ name: z.string(), email: z.string().email() })
const step2 = z.object({ address: z.string() })
const fullSchema = step1.merge(step2)

const nextStep = async () => {
  const isValid = await trigger(['name', 'email']) // Validate specific fields
  if (isValid) setStep(2)
}

条件验证:

z.discriminatedUnion('accountType', [
  z.object({ accountType: z.literal('personal'), name: z.string() }),
  z.object({ accountType: z.literal('business'), companyName: z.string() }),
])

使用 shouldUnregister 的条件字段:

const form = useForm({
  resolver: zodResolver(schema),
  shouldUnregister: false, // Keep values when fields unmount (default)
})

// Or use conditional schema validation:
z.object({
  showAddress: z.boolean(),
  address: z.string(),
}).refine((data) => {
  if (data.showAddress) {
    return data.address.length > 0;
  }
  return true;
}, {
  message: "Address is required",
  path: ["address"],
})

shadcn/ui 集成

注意:shadcn/ui 已弃用 Form 组件。新实现请使用 Field 组件(查看最新文档)。

常见导入错误:IDE/AI 可能会从 "react-hook-form" 而非 shadcn 自动导入 Form。请始终按以下方式导入:

// ✅ Correct:
import { useForm } from "react-hook-form";
import { Form, FormField, FormItem } from "@/components/ui/form"; // shadcn

// ❌ Wrong (auto-import mistake):
import { useForm, Form } from "react-hook-form";

旧版 Form 组件:

<FormField control={form.control} name="username" render={({ field }) => (
  <FormItem>
    <FormControl><Input {...field} /></FormControl>
    <FormMessage />
  </FormItem>
)} />

性能

  • 对于标准输入控件,优先使用 register(非受控),而非 Controller(受控)
  • 使用 watch('email'),而非 watch()(将重新渲染限制在特定字段)
  • 多步表单使用 shouldUnregister: true(卸载时清除数据)
大型表单(300+ 个字段)

警告:使用 resolver(Zod/Yup)并读取 formState 属性的表单,在注册 300+ 个字段时可能会卡顿 10–15 秒。(问题 #13129)

性能表现:

  • 清爽(无 resolver,不读取 formState):几乎即时
  • 仅使用 resolver:几乎即时
  • 仅读取 formState:几乎即时
  • 同时使用 resolver + 读取 formState:300 个字段约需 9.5 秒

解决方法:

  1. 避免解构 formState - 仅在需要时内联读取属性:
// ❌ Slow with 300+ fields:
const { isDirty, isValid } = form.formState;

// ✅ Fast:
const handleSubmit = () => {
  if (!form.formState.isValid) return; // Read inline only when needed
};
  1. 使用 mode: "onSubmit" - 不要在每次更改时都验证:
const form = useForm({
  resolver: zodResolver(largeSchema),
  mode: "onSubmit", // Validate only on submit, not onChange
});
  1. 拆分为子表单 - 使用独立的 schema 创建多个较小的表单:
// Instead of one 300-field form, use 5-6 forms with 50-60 fields each
const form1 = useForm({ resolver: zodResolver(schema1) }); // Fields 1-50
const form2 = useForm({ resolver: zodResolver(schema2) }); // Fields 51-100
  1. 延迟渲染字段 - 使用标签页/折叠面板,仅挂载可见字段:
// Only mount fields for active tab, reduces initial registration time
{activeTab === 'personal' && <PersonalInfoFields />}
{activeTab === 'address' && <AddressFields />}

关键规则

✅ 始终设置 defaultValues(避免非受控→受控警告)

✅ 同时进行客户端和服务端验证(客户端验证可被绕过 — 注意安全!)

✅ 在 useFieldArray 中将 field.id 用作 key(而非索引)

✅ 在 Controller 的 render 中展开 {...field}

✅ 使用 z.infer<typeof schema> 推断类型

❌ 绝不可跳过服务端验证(安全漏洞)

❌ 绝不要直接修改值(使用 setValue())

❌ 绝不要混用受控与非受控模式

❌ 在 useFieldArray 中绝不要使用索引作为 key


已知问题(已预防 20 项)

  1. Zod v4 类型推断 - #13109:显式使用 z.infer<typeof schema>。已在 v7.66.x+ 中解决。注意:@hookform/resolvers 与 Zod v4 存在 TypeScript 兼容性问题(#813)。解决方法:使用 import { z } from 'zod/v3',或等待 resolver 更新。

  2. 非受控→受控警告 - 始终为所有字段设置 defaultValues

  3. 嵌套对象错误 - 使用可选链:errors.address?.street?.message

  4. 数组字段重新渲染 - 在 useFieldArray 中使用 key={field.id}(而不是 index)

  5. 异步验证竞态条件 - 对验证进行防抖处理,并取消待处理请求

  6. 服务器错误映射 - 使用 setError() 将服务器错误映射到字段

  7. 默认值未应用 - 在 useForm 选项中设置 defaultValues(而不是 useState)

  8. Controller 字段未更新 - 始终在 render 函数中展开 {...field}

  9. useFieldArray 键警告 - 使用 field.id 作为键(而不是 index)

  10. Schema 精化错误路径 - 在精化中指定 path:refine(..., { path: ['fieldName'] })

  11. Transform 与 Preprocess - 输出使用 transform,输入使用 preprocess

  12. 多个 Resolver 冲突 - 使用单个 resolver(zodResolver),如有需要可合并 schema

  13. Zod v4 可选字段 Bug - #13102:将可选字段(.optional())设为空字符串 "" 会错误地触发验证错误。解决方法:使用 .nullish()、.or(z.literal("")),或 z.preprocess((val) => val === "" ? undefined : val, z.email().optional())

  14. useFieldArray 不支持基本类型数组 - #12570:这是设计限制。useFieldArray 仅适用于对象数组,不适用于 string[] 等基本类型数组。解决方法:将基本类型包装在对象中:使用 [{ value: "string" }],而不是 ["string"]

  15. useFieldArray SSR ID 不匹配 - #12782:SSR(Remix、Next.js)时会出现 Hydration 不匹配警告。服务器生成的字段 ID 与客户端不匹配。解决方法:仅在客户端渲染字段数组,或等待 V8(使用确定性的 key)

  16. Next.js 16 reset() 验证 Bug - #13110:Server Actions 提交后调用 form.reset(),会导致下一次提交时出现验证错误。已在 v7.65.0+ 中修复。修复前:使用 setValue() 代替 reset()

  17. 验证竞态条件 - #13156:resolver 验证期间会出现中间渲染,此时 isValidating=false,但尚未填充 errors。不要仅根据 errors 推断有效性。使用:!errors.field && !isValidating

  18. Beta 版本中抛出 ZodError - #12816:Zod v4 beta 版本会直接抛出 ZodError,而不是将其捕获到 formState.errors 中。已在稳定版 Zod v4.1.x+ 中修复。避免使用 beta 版本

  19. 大型表单性能 - #13129:300+ 个字段搭配 resolver 和 formState 读取时,会卡顿 10-15 秒。请参阅 Performance 部分中的 4 种解决方法

  20. shadcn Form 导入混淆 - IDE/AI 可能会从 "react-hook-form" 自动导入 Form,而不是从 shadcn 导入。务必从 @/components/ui/form 导入 Form 组件


V8 即将推出的变更(Beta)

React Hook Form v8(截至 v8.0.0-beta.1,目前处于 Beta 阶段,于 2026-01-11 发布)引入了破坏性变更。RFC 讨论 #7433

破坏性变更:

  1. useFieldArray:id → key:
// V7:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.id}>...</div>)

// V8:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.key}>...</div>)
// keyName prop removed
  1. Watch 组件:names → name:
// V7:
<Watch names={["email", "password"]} />

// V8:
<Watch name={["email", "password"]} />
  1. 移除 watch() 回调 API:
// V7:
watch((data, { name, type }) => {
  console.log(data, name, type);
});

// V8: Use useWatch or manual subscription
const data = useWatch({ control });
useEffect(() => {
  console.log(data);
}, [data]);
  1. setValue() 不再更新 useFieldArray:
// V7:
setValue("items", newArray); // Updates field array

// V8: Must use replace() API
const { replace } = useFieldArray({ control, name: "items" });
replace(newArray);

V8 优势:

  • 修复 SSR 水合不匹配问题(使用确定性的 key 替代随机 id)
  • 性能提升
  • 更好的 TypeScript 类型推断

迁移时间表:V8 目前处于 Beta 阶段。稳定版发布日期待定。请关注版本发布以了解稳定版的发布情况。


捆绑资源

模板:basic-form.tsx、advanced-form.tsx、shadcn-form.tsx、server-validation.ts、async-validation.tsx、dynamic-fields.tsx、multi-step-form.tsx、package.json

参考资料:zod-schemas-guide.md、rhf-api-reference.md、error-handling.md、performance-optimization.md、shadcn-integration.md、top-errors.md

文档:https://react-hook-form.com/ | https://zod.dev/ | https://ui.shadcn.com/docs/components/form


许可证:MIT | 上次验证时间:2026-01-20 | 技能版本:2.1.0 | 变更:新增 8 个已知问题(Zod v4 可选字段错误、useFieldArray 原始值限制、SSR 水合不匹配、大型表单的性能建议、Next.js 16 reset() 错误、验证竞态条件、Beta 版中抛出 ZodError、shadcn 导入混淆);新增 Zod v4.3.0 功能(.exactOptional()、.xor()、z.fromJSONSchema());新增使用 shouldUnregister 的条件字段模式;新增 V8 Beta 版破坏性变更章节;扩展 Zod v4 解析器兼容性说明;更新至 [email protected]

评论

支持 Markdown