mcpField
R
react-hook-form-zod

react-hook-form-zod

github.com/secondsky开发242386 次安装5 周前更新
代码仓库

关于 react-hook-form-zod

面向 React 表单开发者的 React Hook Form 与 Zod 指南,提供类型化验证模式、示例模板和故障排查资料,涵盖客户端与服务端验证。

主要功能

  1. 01React Hook Form 与 Zod 配置
  2. 02基于 Schema 的类型推断
  3. 03动态及多步骤表单模式
  4. 04异步与服务端验证
  5. 05无障碍与性能指导
  6. 06模板和故障排查资料

适用场景

  1. 01实现登录或注册表单
  2. 02添加动态字段或多步骤表单
  3. 03在客户端和服务端验证输入
  4. 04排查 Resolver 与类型错误

安装

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

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

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

包含的文件

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

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
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.tsx
shadcn-form.tsx

详细介绍

React Hook Form + Zod 验证

状态:已可用于生产环境 ✅ 最后更新:2025-11-21 依赖项:无(独立运行) 最新版本:[email protected]、[email protected]、@hookform/[email protected]


快速开始(10 分钟)

1. 安装软件包
bun add [email protected] [email protected] @hookform/[email protected]

为什么需要这些软件包:

  • 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. Define validation schema
const loginSchema = z.object({
  email: z.email({ error: 'Invalid email address' }),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

// 2. Infer TypeScript type from schema
type LoginFormData = z.infer<typeof loginSchema>

function LoginForm() {
  // 3. Initialize form with zodResolver
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  // 4. Handle form submission
  const onSubmit = async (data: LoginFormData) => {
    // Data is guaranteed to be valid here
    console.log('Valid data:', data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">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">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 ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

重要:

  • 始终设置 defaultValues,以避免出现「非受控变为受控」警告
  • 使用 zodResolver(schema) 接入 Zod 验证
  • 使用 z.infer<typeof schema> 为表单添加类型,确保完整的类型安全
  • 在客户端和服务器端都进行验证(绝不要只相信客户端验证)

模板:参见 templates/basic-form.tsx,查看完整的可运行示例

3. 添加服务器端验证
// server/api/login.ts
import { z } from 'zod'

// SAME schema on server
const loginSchema = z.object({
  email: z.email({ error: 'Invalid email address' }),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

export async function loginHandler(req: Request) {
  try {
    const data = loginSchema.parse(await req.json())
    // Data is type-safe and validated
    return { success: true }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: z.flattenError(error).fieldErrors }
    }
    throw error
  }
}

为什么要进行服务器端验证:

  • 客户端验证可以被绕过(检查元素、Postman、curl)
  • 服务器端验证是你的安全防护层
  • 使用相同的 Zod 架构 = 单一事实来源

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


核心概念

useForm Hook
const {
  register,           // Register input fields
  handleSubmit,       // Wrap onSubmit handler
  formState,          // Form state (errors, isValid, isDirty, etc.)
  setValue,           // Set field value programmatically
  getValues,          // Get current form values
  watch,              // Watch field values
  reset,              // Reset form to defaults
  trigger,            // Trigger validation manually
  control,            // For Controller/useController
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onSubmit',               // When to validate
  defaultValues: {},              // Initial values (REQUIRED)
})

验证模式:

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

参考:参见 references/rhf-api-reference.md,查看完整 API

Zod 架构基础
import { z } from 'zod'

// Basic types
const schema = z.object({
  email: z.email({ error: 'Invalid email' }),
  age: z.number().min(18, 'Must be 18+'),
  terms: z.boolean().refine(val => val === true, 'Must accept terms'),
})

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

// Arrays
const tagsSchema = z.object({
  tags: z.array(z.string()).min(1, 'At least one tag required'),
})

// Optional and nullable
const optionalSchema = z.object({
  middleName: z.string().optional(),
  nickname: z.string().nullable(),
  bio: z.string().nullish(), // optional AND nullable
})

参考:参见 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:非受控变为受控警告 ⚠️

错误:

Warning: A component is changing an uncontrolled input to be controlled

原因:未设置 defaultValues

解决方案:

// ❌ BAD
const form = useForm()

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

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

**错误:**类型推断无法正常工作

解决方案:

// Explicitly type useForm if needed
const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})

来源:GitHub Issue #13109


错误 #3:未找到 Resolver

错误:

Module not found: Can't resolve '@hookform/resolvers/zod'

解决方案:

# Install the resolvers package
bun add @hookform/[email protected]

错误 #4:数组字段问题

**错误:**动态数组字段无法与 useFieldArray 配合使用

解决方案:

const { fields, append, remove } = useFieldArray({
  control,
  name: "items" // Must match schema field name exactly
})

模板:参见 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, 'Name required'),
  email: z.email({ error: 'Invalid 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">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, 'At least one item required')
})

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)}>Remove</button>
        </div>
      ))}
      <button onClick={() => append({ name: '', quantity: 1 })}>
        Add Item
      </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
    }, 'Username already taken')
})

模板: 查看 templates/async-validation.tsx


多步骤表单
function MultiStepForm() {
  const [step, setStep] = useState(1)
  const form = useForm({
    resolver: zodResolver(schema),
    mode: 'onBlur' // Validate each step before proceeding
  })

  const onSubmit = async (data) => {
    if (step < 3) {
      setStep(step + 1)
    } else {
      // Final submission
      await submitForm(data)
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {step === 1 && <Step1Fields />}
      {step === 2 && <Step2Fields />}
      {step === 3 && <Step3Fields />}
      <button type="submit">
        {step < 3 ? 'Next' : 'Submit'}
      </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>Email</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.tsx - 使用 Zod 进行服务器端验证
  • custom-error-display.tsx - 自定义错误消息组件
  • package.json - 软件包版本和脚本
参考文档(references/)

详细文档:

  • top-errors.md - 全部 12 种常见错误及其解决方案和来源
  • rhf-api-reference.md - 完整的 React Hook Form API 参考
  • zod-schemas-guide.md - 全面的 Zod 模式
  • shadcn-integration.md - shadcn/ui Form 集成指南
  • error-handling.md - 错误显示模式和无障碍性
  • performance-optimization.md - 重新渲染优化策略
  • accessibility.md - WCAG 合规性和屏幕阅读器支持
  • links-to-official-docs.md - 整理后的官方文档链接

何时加载参考文档

参考文档加载时机……
top-errors.md调试验证问题、类型错误或“非受控转受控”警告时
rhf-api-reference.md需要了解 useForm、register、Controller、formState 的完整 API 时
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)

常见模式:

// Email
z.email({ error: 'Invalid email' })

// Password (min 8 chars, 1 uppercase, 1 number)
z.string()
  .min(8)
  .regex(/[A-Z]/, 'Need uppercase')
  .regex(/[0-9]/, 'Need number')

// URL
z.url({ error: 'Invalid URL' })

// Date
z.datetime() // ISO 8601
z.date() // JS Date object

// File upload
z.instanceof(File)
  .refine(file => file.size <= 5000000, 'Max 5MB')
  .refine(
    file => ['image/jpeg', 'image/png'].includes(file.type),
    'Only JPEG/PNG allowed'
  )

// Custom validation
z.string().refine(
  val => val !== 'admin',
  'Username "admin" is reserved'
)

// Async validation
z.string().refine(
  async (username) => {
    const available = await checkUsername(username)
    return available
  },
  'Username already taken'
)

参考: 查看 references/zod-schemas-guide.md,了解所有模式


依赖项

必需:

可选:

  • @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

找不到 Resolver 错误

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

自定义组件无法验证

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

表单重新渲染过多

解决方案:使用 mode: 'onBlur' 和 useWatch → 查看 references/performance-optimization.md


生产环境示例

此技能基于生产环境中的实践模式:

  • 真实场景中的表单:登录、注册、结账、多步骤向导
  • 验证:客户端和服务端使用共享的 Zod Schema
  • 无障碍性:符合 WCAG 2.1 AA 标准
  • 性能:优化以尽量减少重新渲染

Token 节省:约 60%(包含模板的全面表单模式) 错误预防:100%(针对所有 12 个已记录问题提供解决方案) 已准备好用于生产环境! ✅

评论

支持 Markdown