最近在浏览 shadcn/ui 的 表单文档 (Forms)。
时,发现它并没有去造一个全新的表单轮子,而是推荐使用两个神仙库:React Hook Form 和 Zod。

为什么 shadcn/ui 会选择这个组合?我使用下来总结有以下几点:
-
react-hook-form有极致的性能与状态管理
-
Zod对表单校验规则的全面封装
在表单场景中他俩就是天作之合、强强联手
1. React Hook Form:高性能的表单状态管理
React Hook Form(RHF)是一个 轻量、高性能的 React 表单库。
它的核心理念是:
让表单尽可能接近原生 HTML form。
相比传统的表单写法:
js
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
受控组件(Controlled Components)每次输入都会触发整个组件的重渲染。
React Hook Form (RHF) 采用了非受控组件的思想,用 ref 直接读取表单项的值。
React Hook Form 的核心 API
React Hook Form 的 API 非常少,最核心的是以下几个。
1 useForm
useForm 用于创建表单实例。
ts
const {
register,
handleSubmit,
control,
reset,
watch,
formState
} = useForm()
其中:
register:注册字段handleSubmit:处理提交control:控制复杂组件formState:表单状态
2 register
用于注册input字段,它会返回 onChange、onBlur、name 和 ref。你可以直接用解构语法 {...register('字段名')} 绑定到原生 上
jsx
<input {...register("email")} />
这行代码实际上会:
- 注册字段到表单
- 绑定 value、onChange、ref
提交时 React Hook Form 会自动收集数据:
json
{
"email": "example@email.com"
}
3 handleSubmit
处理表单提交。
jsx
<form onSubmit={handleSubmit(onSubmit)}>
ts
const onSubmit = (data) => {
console.log(data)
}
4 formState
formState 用于获取表单状态,有errors,isSubmitting,isValid,dirtyFields,touchedFields等状态
例如显示表单校验错误信息:
jsx
<p>{form.formState.errors.email?.message}</p>
二、Zod:类型安全的 schema validation
表单校验需要一套规则,而 Zod 就是用来定义这些规则的。
你只需要定义一次 Zod Schema,它就能同时为你提供运行时校验和编译时的类型提示,告别重复写 TS 接口和校验逻辑。
例如:
ts
import { z } from "zod"
const UserSchema = z.object({
name: z.string(),
age: z.number(),
})
UserSchema.parse(data)
如果数据不符合 schema,就会抛出错误。
Zod 常用 API
- 字符串校验
ts
z.string()
z.string().min(3)
z.string().max(20)
z.string().email()
z.string().url()
例如:
ts
z.string().min(3, "Too short")
- number
ts
z.number().min(0)
- optional
ts
z.string().optional()
- enum
ts
z.enum(["todo", "doing", "done"])
- object
ts
z.object({
title: z.string(),
description: z.string()
})
类型推导
z.infer<typeof schema>是 Zod 最"魔法"的 API! 它可以直接根据你写的 Schema 反向推导出 TypeScript 的 type 或 interface.比如在我的项目中:
ts
const formSchema = z
.object({
title: z.string().trim().min(1, "Title is required."),
description: z.string().trim().optional(),
start: z.date().optional(),
end: z.date().optional(),
})
.refine(
(v) => {
if (!v.start || !v.end) return true;
return v.end.getTime() >= v.start.getTime();
},
{
path: ["end"],
message: "End must be after start.",
}
);
type FormData = z.infer<typeof formSchema>
三、一个完整表单示例
下面是一个包含"用户名"和"邮箱"的基础表单组件示例:
ts
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// 1. 使用 Zod 定义表单的数据模式 (Schema)
const formSchema = z.object({
username: z.string().min(2, { message: "用户名至少需要 2 个字符" }),
email: z.string().email({ message: "请输入有效的邮箱地址" }),
});
// 2. 利用 Zod 自动推导 TypeScript 类型
type FormData = z.infer<typeof formSchema>;
export default function MinimalForm() {
// 3. 初始化 React Hook Form
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema), // 绑定 Zod 校验规则
});
// 4. 提交处理函数
const onSubmit = (data: FormData) => {
console.log("验证通过的数据:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: '1rem', width: '300px' }}>
{/* 用户名输入框 */}
<div>
<label>用户名</label>
<input {...register('username')} placeholder="输入用户名" />
{errors.username && <p style={{ color: 'red' }}>{errors.username.message}</p>}
</div>
{/* 邮箱输入框 */}
<div>
<label>邮箱</label>
<input {...register('email')} placeholder="输入邮箱" />
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
</div>
<button type="submit">提交</button>
</form>
);
}
总结
shadcn/ui推荐这套方案不是没有原因的。React Hook Form 解决了"怎么高效收集和管理状态"的问题,而 Zod 解决了"怎么优雅定义规则和类型"的问题。