ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在 Refine 应用中使用 Zod 与 React Hook Form 实现类型安全的表单 Schema 校验

在 Refine 应用中使用 Zod 与 React Hook Form 实现类型安全的表单 Schema 校验 在 Refine 应用中使用 Zod 与 React Hook Form 实现类型安全的表单 Schema 校验【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refineZod 是 TypeScript-first 的 Schema 声明与校验库它与 React Hook FormRHF通过hookform/resolvers的zodResolver无缝集成让表单字段校验从手写规则升级为声明式类型安全 Schema。本文以 Refinerefinedev/react-hook-form数据驱动表单及其生态中的实践为例完整演示如何将 Zod 的object()、string()、min()、max()、email()、default()、refine()、transform()、infer()、parse()等核心 API 应用于真实表单并带领读者将一个基于纯 React Hook Form 的Create Post表单逐步迁移到 Zod 校验体系。为什么表单需要 Schema 校验表单 Schema 的本质是对表单所处理数据实体的形状shape做显式定义。在一个以资源resource为中心的前端应用中Post、User、Category等数据实体都会对应到表单字段。随着实体数量增长类型重叠、映射、互相转换、派生等操作不可避免完全靠手写静态类型会让表单变得臃肿且难以维护。Schema 校验带来的收益是结构性的关注点集中Schema 聚焦于表单所处理的数据实体字段声明与校验规则内聚在同一处DRY 原则一套 Schema 可同时服务于表单、类型推导、服务端通信避免重复声明稳定性与可维护性实体增加时Schema 让声明保持一致降低回归风险。在 TypeScript 生态中Zod 之所以区别于 Yup、Joi 等同类库核心在于其静态类型 API 表面与 TypeScript 本身的类型声明、推导、衍生工具高度镜像你在 Schema 上做的每一次partial()、pick()、omit()、infer()都能同步产生对应的静态类型从而让数据本身和类型系统始终保持一致这对日益增长的大型 TypeScript 代码库尤为重要。Zod 与 React Hook Form 的集成机制Zod 的核心工作模型在深入代码前先梳理 Zod 的几个基本概念后续所有示例都建立在这套模型之上概念说明典型 APIZod 实例通过zod对象暴露校验器声明 APIimport * as zod from zod校验器Validator单个校验声明代表数据实体中的一个属性zod.string()原语Primitive对应 JS/TS 基本类型的声明方法string()、number()、boolean()对象 Schema用object()组合原语代表整个数据实体zod.object({...})精度校验器在原语上追加的精确规则min()、max()、email()Schema 派生支持完整 TypeScript 支持的类型操作partial()、pick()、omit()细化Refinement自定义精细校验规则refine()、superRefine()变换Transformation转换字段值transform()类型生成从 Schema 生成静态类型infer()校验运行执行校验并返回结果parse()、safeParse()Zod Resolver连接 Zod 与 React Hook FormZod 本身不感知 React Hook Form 的存在两者之间需要一个桥梁——Zod Resolver。它来自hookform/resolvers集成包职责是监听 React Hook Form 中的校验事件、执行 Zod 校验、把成功或失败的结果翻译回 React Hook Form 的formState。npm install zod hookform/resolvers安装后在useForm()中通过resolver配置项接入import { zodResolver } from hookform/resolvers/zod; import * as zod from zod; const formInstance useForm({ resolver: zodResolver(PostSchema), mode: onChange, // ...其他配置 });注意只安装zod而不安装hookform/resolvers无法与 React Hook Form 协同工作zodResolver是集成环节中不可或缺的一环。实战一将纯 React Hook Form 表单迁移到 Zod下面以一个功能完整的Create Post表单为例演示迁移全过程。迁移前表单使用 React Hook Form 原生校验规则通过register()在每个字段上声明规则。迁移前的表单结构const formInstance useForm({ mode: onChange, defaultValues: { title: , subtitle: , content: , }, criteriaMode: all, shouldFocusError: true, });其中关键配置的含义mode: onChange每次值变化时触发校验defaultValues提供默认值同时让 TypeScript 推断整个表单的数据形状即隐式 SchemacriteriaMode: all校验时返回全部规则错误而非仅第一个shouldFocusError: true出现错误时自动聚焦第一个错误字段。字段层面使用原生规则例如title的必填、subtitle的最大长度、content的最小/最大长度input {...formInstance?.register(title, { required: Post title cannot be empty, })} typetext placeholderAdd post title /提交时handleSubmit回调里通常执行数据请求fetch()、React Query mutation 或 Axios 的 POST/PUT/PATCH。示例中为了演示服务端错误集成模拟了一个 2 秒后返回的subtitle字段服务端错误onSubmit{formInstance?.handleSubmit((data) { setTimeout(() { console.log(data, data); formInstance?.setError(subtitle, { message: new Error(Server Error: Subtitle field is protected).message, }); }, 2000); })}迁移后声明 Zod Schema 并接入 resolver迁移后校验规则从 JSX 中抽离到PostSchema对象中字段的register()不再携带任何规则import { useEffect } from react; import { useForm } from react-hook-form; import * as zod from zod; import { zodResolver } from hookform/resolvers/zod; import ./App.css; function App() { const subtitle zod.string().max(65, { message: Keep subtitle shorter }); const content zod .string() .min(20, { message: Content should have enough information }) .max(1000, { message: Content has reached maximum limit of 1000 characters, }); const PostSchema zod.object({ title: zod.string().min(1, { message: Title cannot be empty }), subtitle, content, }); type TPost zod.infertypeof PostSchema; const formInstance useForm({ resolver: zodResolver(PostSchema), mode: onChange, defaultValues: { title: , subtitle: , content: , }, criteriaMode: all, shouldFocusError: true, }); // ...JSX 中字段仅需注册不再传规则 return ( // ... input {...formInstance?.register(title)} typetext placeholderAdd post title / // ... ); }注意 JSX 中的关键变化{...formInstance?.register(title)}不再传入required等规则因为校验职责已全部交给zodResolver。而handleSubmit中的服务端错误集成setError等 React Hook Form 特性完全不受影响。用object()组合实体 Schemazod.object()用于初始化一个代表数据实体的对象 Schema其属性即实体的各个字段const PostSchema zod.object({ title: zod.string().min(1, { message: Title cannot be empty }), subtitle, content, });既可以在对象内联声明字段校验器也可以像subtitle、content一样单独提取出来复用再组合进 Schema。声明校验器原语 精度校验器string()是 Zod 原语方法代表 TypeScript 的string类型决定字段的静态类型。在原语之上可以通过min()、max()、email()等精度校验器追加规则语法为原语 链式精度校验器且精度校验器遵循统一语法——先传规格值再传携带message的错误信息对象const content zod .string() .min(20, { message: Content should have enough information }) .max(1000, { message: Content has reached maximum limit of 1000 characters, });通过上述迁移我们获得了与原生 RHF 校验完全一致的行为——用声明式 Schema 替换了分散在 JSX 里的规则这是 Zod 在工程可维护性上带来的直接收益。parse()与safeParse()校验的运行时机Zod 的校验运行默认由zodResolver根据 React Hook Form 的mode与reValidateMode策略触发。除此之外也可以在任何地方手动运行校验PostSchema.safeParse({ title: General Zod of Candor, subtitle: Executing..., content: Kneel before Kal El., }); // 返回 { success: true, data: {...} } PostSchema.safeParse({ title: General Zod of Candor, subtitle: Executing... Running now and forever, content: Kneel, }); // 返回 { success: false, error: [...] }safeParse()不会抛出异常数据通过校验时返回{ success: true, data }未通过时返回{ success: false, error }应用不会被中断。⚠️ 警告parse()与safeParse()不同它在校验失败时会抛出ZodError直接中断应用。如果必须使用parse()请务必用try...catch包裹以优雅处理错误。infer()从 Schema 生成静态类型Zod 会为所有 Schema 生成静态类型使用infer即可提取type TPost zod.infertypeof PostSchema; /* { title: string; subtitle: string; content: string; } */TPost可以继续用于注释表单提交数据、API 请求体等场景实现一处声明、多处复用。实战二EditProfile /中的进阶 Zod 特性第二个示例聚焦EditProfile表单覆盖默认值、Schema 派生、细化校验与值变换四个进阶能力。这里先给出核心 Schema 声明后续逐点拆解export function EditProfile() { const first_name zod .string() .min(1, { message: First name cannot be empty }) .max(50, { message: Really? This long ? }) .default(Dru); const last_name zod .string() .min(1, { message: Last name cannot be empty }) .max(50, { message: Really? This long ? }) .default(Zod); const email zod .string() .email() .refine((e) e.slice(e.length - 3).includes(.kr), { message: This should be a Kryptonian email, }) .default(general.zodcandor.mil.kr); const ProfileSchema zod.object({ username: zod .string() .transform((u) u.split( ).join(_)) .default(general_zod), first_name, last_name, email, }); const formInstance useForm({ resolver: zodResolver(ProfileSchema), mode: onChange, defaultValues: ProfileSchema.parse({}), criteriaMode: all, shouldFocusError: true, reValidateMode: onSubmit, }); // ... }default()在 Schema 上声明默认值Zod 的默认值直接声明在校验器上const first_name zod .string() .min(1, { message: First name cannot be empty }) .max(50, { message: Really? This long ? }) .default(Dru);注意这些默认值不会自动同步到 React Hook Form 的defaultValues因此不会直接显示在表单字段上。要让它们生效需要先对 Schema 执行一次解析再把输出传给useForm()const formInstance useForm({ resolver: zodResolver(ProfileSchema), mode: onChange, defaultValues: ProfileSchema.parse({}), criteriaMode: all, shouldFocusError: true, reValidateMode: onSubmit, });ProfileSchema.parse({})传入空对象时Schema 中通过default()声明的值会作为默认值被填充到对应字段。partial()/pick()/omit()Schema 派生Schema 派生可以按需生成新 Schema 与新类型行为与 TypeScript 内置工具类型一一对应const ProfileOptional ProfileSchema.partial(); const ProfileOptionalLastName ProfileSchema.partial({ last_name: true, }); type TProfileOptional zod.infertypeof ProfileOptional; /* type TProfileOptional { username?: string | undefined; first_name?: string | undefined; last_name?: string | undefined; email?: string | undefined; }; */ type TProfileOptionalLastName zod.infertypeof ProfileOptionalLastName; /* type TProfileOptionalLastName { username: string; first_name: string; email: string; last_name?: string | undefined; }; */无参数调用partial()时所有字段变为可选传入{ last_name: true }则只将指定字段变为可选。同理pick()对应 TypeScript 的Pickomit()对应Omit。这些派生在创建表单可省略部分字段编辑表单部分字段只读等真实场景中非常实用省去了大量重复的类型声明。refine()自定义精细化校验规则原语与精度校验器无法覆盖所有业务规则此时用refine()实现自定义校验。例如要求邮箱不仅合法、且必须是.kr结尾的氪星邮箱const email zod .string() .email() .refine((e) e.slice(e.length - 3).includes(.kr), { message: This should be a Kryptonian email, }) .default(general.zodcandor.mil.kr);refine()接收一个返回布尔值的校验函数第二参数提供错误信息。对于需要同时报告多个问题、更细粒度控制错误路径的复杂场景可以使用更冗长但表达能力更强的superRefine()方法。transform()字段值变换变换允许在校验通过后改写字段值。例如将username中的空格统一替换为下划线const ProfileSchema zod.object({ username: zod .string() .transform((u) u.split( ).join(_)) .default(general_zod), first_name, last_name, email, });变换后的数据会写入formInstance。提交表单后在handleSubmit回调的 console 输出中可以验证username不再包含空格全部变成下划线例如用户输入 general dru zod 时最终提交数据为{ username: general_dru_zod, first_name: Dru, last_name: Zod, email: general.zodcandor.mil.kr }在 Refine 中的落地refinedev/react-hook-form与 ZodRefine 对 React Hook Form 做了封装提供refinedev/react-hook-form包。其useForm在内部直接调用 React Hook Form 的useForm并将resolver等配置原样透传——你可以在 packages/react-hook-form/src/useForm/index.ts 中看到useHookForm({ ...rest })的透传实现同时额外注入了refineCore数据提供器集成、saveButtonProps等 Refine 特性。因此在 Refine 应用中接入 Zod 与纯 RHF 完全一致只需安装npm install refinedev/react-hook-form hookform/resolvers zod然后在useForm中传入zodResolver。以 Refine 官方文档的 shadcn/ui 表单指南documentation/docs/ui-integrations/shadcn/components/forms/index.md为例还可以用z.enum()声明枚举字段import * as z from zod; const postSchema z.object({ title: z.string().min(2, Title must be at least 2 characters), content: z.string().min(10, Content must be at least 10 characters), status: z.enum([draft, published, rejected], { errorMap: () ({ message: Please select a status }), }), }); type PostFormData z.infertypeof postSchema;创建表单时通过refineCore.onFinish提交数据Refine 会自动调用数据提供器的 create 方法const { refineCore: { onFinish, formLoading }, ...form } useFormBaseRecord, HttpError, PostFormData({ resolver: zodResolver(postSchema), defaultValues: { title: , content: , status: draft, }, refineCoreProps: { resource: posts, action: create, }, }); const onSubmit (data: PostFormData) { onFinish(data); // 自动调用数据提供器的 create 方法 };Zod 推导出的PostFormData同时约束了表单泛型与提交数据让Schema → 类型 → 表单 → 数据提供器整条链路类型一致。仓库中的 examples/form-react-hook-form-use-form/src/pages/posts/create.tsx 展示了 Refine 表单的标准形态useFormrefineCore.onFinish 字段注册在其基础上将register规则替换为zodResolver即可获得完整的 Zod 校验能力更多 React Hook Form 状态管理基础可参考 documentation/blog/2024-11-06-react-hook-form.md。小结本文围绕 Zod 与 React Hook Form 的 Schema 校验集成完成了从理论到实战的完整梳理Schema 的价值以数据实体为中心声明表单形状带来 DRY、稳定性与可扩展性Zod 通过镜像 TypeScript 类型 API 的静态类型表面成为大型 TypeScript 代码库中的优选方案基础迁移路径通过zod.object()组合string()等原语与min()、max()、email()精度校验器配合zodResolver接入useForm()即可将纯 RHF 表单迁移为 Schema 校验表单运行时校验safeParse()安全地手动触发校验parse()需配合try...catch使用infer()负责从 Schema 生成静态类型进阶能力default()声明默认值并配合ProfileSchema.parse({})注入表单partial()、pick()、omit()实现 TypeScript 同语义的 Schema 派生refine()/superRefine()实现精细化自定义规则transform()在提交前变换字段值Refine 落地refinedev/react-hook-form的useForm透传resolver让 Zod 校验能力与数据提供器、通知系统等 Refine 特性无缝共存。当表单校验规则开始散落在 JSX 各处、或类型与校验重复维护时用 Zod Schema 把它们收敛到一处是保持代码库长期健康的务实选择。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表