关于 react-hook-form-zod-shadcn
面向 React 表单开发的实现指南,组合 react-hook-form、Zod 和 shadcn/ui。强调实时校验、类型匹配的默认值及提交期间禁用表单。
主要功能
- 01react-hook-form 表单集成
- 02使用 Zod 定义校验模式
- 03遵循 shadcn/ui Field 结构
- 04实时校验与默认值配置
- 05提交期间禁用字段和按钮
适用场景
- 01搭建带校验的 React 表单
- 02实现输入时即时反馈
- 03阻止提交期间的重复操作
- 04按统一模式生成表单组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sortweste/ frontend-skills --skill react-hook-form-zod-shadcn安装后在 Claude Code 中输入/react-hook-form-zod-shadcn确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdreferences/react-hook-form.mdshadcn-ui.mdzod.md详细介绍
React Hook Form + Zod + shadcn/ui
快速开始
npm install [email protected] [email protected] @hookform/[email protected]
表单组件文件顶部始终添加 'use client'。
"use client";
import { useForm } from "react-hook-form";
// ... other imports
export function MyForm() {
// Component logic
}
所有表单都必须使用 shadcn/ui Field 组件模式,并与 react-hook-form 集成。
"use client";
import { useForm } from "react-hook-form";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
export function MyForm() {
const form = useForm();
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel>Title</FieldLabel>
<Input
{...field}
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</FieldGroup>
</form>
);
}
为提升性能,请在组件外定义架构。使用 Zod 定义架构。
"use client";
import * as z from "zod";
import { useForm } from "react-hook-form";
const formSchema = z.object({
name: z.string().trim().min(5, { error: "Must be at least 5 characters." }),
description: z.string().trim().min(20, { error: "Must be at least 20 characters." }),
.string()
.trim()
.min(20, { error: "Must be at least 20 characters." }),
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const form = useForm<FormData>();
// ...
}
使用 React 的 useId Hook 为每个表单生成一个唯一 ID。切勿为每个字段分别生成 ID。只生成一次,并与字段名一起重复使用。
"use client";
import { useId } from "react";
import { useForm } from "react-hook-form";
export function MyForm() {
const formId = useId(); // Generate once
const form = useForm();
return (
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
<FieldGroup>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={`${formId}-title`}>Title</FieldLabel>
<Input
{...field}
id={`${formId}-title`} // Use it with template literals for each field.
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
disabled={form.formState.isSubmitting}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</FieldGroup>
</form>
);
}
始终在 useForm Hook 配置中添加 mode: "onChange",以便实时反馈验证结果。
const form = useForm<FormData>({
resolver: standardSchemaResolver(formSchema),
mode: "onChange", // Validates on every change
});
使用与架构预期类型匹配的 defaultValues 初始化表单。
const form = useForm<FormData>({
resolver: standardSchemaResolver(formSchema),
mode: "onChange",
defaultValues: {
username: "",
email: "",
},
});
重要:由于存在一个尚未解决的问题,请始终从 @hookform/resolvers/standard-schema 导入,但使用 standard-schema resolver 模式。
"use client";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
const formSchema = z.object({
//...
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: standardSchemaResolver(formSchema),
mode: "onChange",
});
}
当 form.formState.isSubmitting 为 true 时,禁用提交按钮和所有表单字段,以防止重复提交并提升用户体验。
return (
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
<FieldGroup>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={`${formId}-email`}>email</FieldLabel>
<Input
{...field}
id={`${formId}-email`}
aria-invalid={fieldState.invalid}
placeholder="[email protected]"
autoComplete="off"
disabled={form.formState.isSubmitting}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</FieldGroup>
<Button type="submit" form={formId} disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
);
当表单无效时,使用 form.formState.isValid 禁用提交按钮。
return (
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
{/* Form fields */}
<Button
type="submit"
form={formId}
disabled={!form.formState.isValid || form.formState.isSubmitting}
>
{form.formState.isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
);
重要:始终在表单提交处理程序中使用 safeParse() 验证字段值。如果验证失败,则调用 form.trigger() 显示错误。
"use client";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
const formSchema = z.object({
//...
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: standardSchemaResolver(formSchema),
mode: "onChange",
});
const formId = useId(); // Generate once
const handleSubmit = async (data: FormData) => {
// Validate using safeParse
const result = formSchema.safeParse(data);
if (!result.success) {
// Trigger form validation to show errors
form.trigger();
return;
}
try {
// API call or other submission logic
} catch (error) {
console.error("Submission error:", error);
}
};
return (
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
{/* Form fields */}
</form>
);
}
提交后重置表单。
const handleSubmit = async (data: FormData) => {
//... submission logic
form.reset(); // Reset form after successful submission
};
资源
Zod 参考:请参阅 references/zod.md shadcn/ui 参考:请参阅 references/shadcn-ui.md React Hook Form 参考:请参阅 references/react-hook-form.md