5.1 关键概念
- react-hook-form (RHF)
- 高性能表单库,用非受控为主的策略减少重渲染,通过
useForm管理状态、校验、提交,register/Controller接入字段。 - Zod
- TypeScript 优先的 schema 校验库,用
z.object({...})定义结构与规则,同时能z.infer出 TS 类型,一份 schema 校验+类型两用。 - zodResolver
@hookform/resolvers提供的桥,把 Zod schema 接进 RHF 的resolver,提交/失焦时自动跑 Zod 校验并把错误塞回表单。- shadcn Form
- 一层薄封装(基于 RHF 的 Context),提供
Form/FormField/FormItem/FormLabel/FormControl/FormDescription/FormMessage,自动接线 id、aria、错误展示。 - FormField
- 包裹 RHF
Controller,用name绑定字段、render返回具体输入组件,把field(value/onChange/onBlur/ref)交给你。 - FormMessage
- 自动读取当前字段的校验错误并渲染,无错误则不渲染,还负责设置
aria-describedby关联。 - defaultValues
useForm的初始值,决定字段受控起点,也是"重置表单"的目标;对 TS 类型推断很重要。
5.2 安装
$ npx shadcn@latest add form input button
$ npm i react-hook-form zod @hookform/resolvers
5.3 定义 Zod schema
// schema:一份定义,校验与类型两用
import { z } from "zod";
const formSchema = z.object({
username: z.string().min(2, "至少 2 个字符").max(20),
email: z.string().email("邮箱格式不正确"),
age: z.coerce.number().min(18, "必须年满 18 岁"),
bio: z.string().max(160).optional(),
});
type FormValues = z.infer<typeof formSchema>;
5.4 用 useForm + zodResolver
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { username: "", email: "", age: 18, bio: "" },
});
function onSubmit(values: FormValues) {
// values 已校验且类型安全
console.log(values);
}
5.5 组装 Form JSX
import {
Form, FormControl, FormDescription, FormField,
FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>这会公开显示。</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl><Input type="email" {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
自动接线的魔法
FormField 把 field(value/onChange/onBlur/ref)交给你 spread 进 Input;FormItem 生成唯一 id 并让 Label、Control、Message 通过 aria 属性互相关联;FormMessage 自动显示该字段的 Zod 错误。你几乎不用手写 aria。
5.6 非文本字段:Select / Checkbox
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>角色</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger><SelectValue placeholder="选择角色" /></SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">管理员</SelectItem>
<SelectItem value="user">普通用户</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
受控组件要接 onValueChange
Select/Checkbox/Switch 不是原生 input,不能直接 {...field}。要把 field.onChange 接到 onValueChange/onCheckedChange,把 field.value 接到 value/checked。
5.7 校验时机与模式
const form = useForm({
resolver: zodResolver(schema),
mode: "onBlur", // onSubmit(默认) / onChange / onBlur / onTouched
reValidateMode: "onChange", // 出错后重新校验的时机
});
5.8 跨字段校验与服务端错误
// 跨字段:确认密码
const schema = z.object({
password: z.string().min(8),
confirm: z.string(),
}).refine((d) => d.password === d.confirm, {
message: "两次密码不一致",
path: ["confirm"], // 错误挂到 confirm 字段
});
// 服务端返回的错误手动塞回
form.setError("email", { message: "该邮箱已被注册" });
5.9 提交态与重置
const { isSubmitting, isValid } = form.formState;
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "提交中…" : "注册"}
</Button>
// 提交成功后重置
form.reset();
5.10 小结与下一步
- 一份 Zod schema 同时提供运行时校验与
z.infer出的 TS 类型。 - zodResolver 把 Zod 接进 react-hook-form;shadcn 的 Form 组件自动接线 aria 与错误展示。
- 受控组件(Select/Checkbox)要手动桥接
field.onChange与value。 - 下一章转向布局与导航:Card/Tabs/Accordion/Sheet/Sidebar 搭页面骨架。