news 2026/9/12 2:45:34

Refine v5 中基于 React Hook Form 的 headless 表单实战:useForm 适配器用法与源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 中基于 React Hook Form 的 headless 表单实战:useForm 适配器用法与源码解析

Refine v5 中基于 React Hook Form 的 headless 表单实战:useForm 适配器用法与源码解析

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

Refine 通过@refinedev/react-hook-form适配器,将 React Hook Form 的全部能力以 headless 方式接入 Refine 的数据提供器、导航与变更提醒机制。本文以官方示例 form-react-hook-form-use-form 为主体,讲解useForm的初始化、注册字段、校验、提交、编辑回填、文件上传与自动保存等完整流程,并结合 useForm 实现源码 与 单元测试 剖析其底层工作方式。

一、示例定位与文档背景

documentation/docs/examples/form/react-hook-form/useForm.md是 Refine 文档中「Examples → Form → React Hook Form → useForm」的入口页。它明确指出:Refine 允许你在项目中通过@refinedev/react-hook-form使用 React Hook Form 库的全部特性,并以此构建自己的headless表单——即不带任何 UI 库约束、完全由你自己编写 JSX 的表单。文档同时提供了 live example 与源码链接,并指向 包列表文档。

对应的完整可运行示例位于 examples/form-react-hook-form-use-form,其中src/pages/posts/下包含create.tsx(创建页)、edit.tsx(编辑页)与list.tsx(列表页)三个典型场景,覆盖了一个内部工具最常见的 CRUD 表单需求。

二、快速上手:在本地运行示例

2.1 依赖一览

该示例的package.json(见 examples/form-react-hook-form-use-form/package.json)展示了 headless 表单的最小依赖组合:

  • @refinedev/core:Refine 核心,提供useForm的底层数据逻辑(useFormCore)、useSelectuseTableuseApiUrluseBackuseNavigation等;
  • @refinedev/react-hook-form:核心适配器,本篇文章的主角;
  • @refinedev/simple-rest:REST 风格数据提供器;
  • @refinedev/react-router:路由集成;
  • react-hook-form经由适配器间接使用(适配器内部直接import { useForm as useHookForm } from "react-hook-form");
  • axios:示例中用于文件上传的 HTTP 客户端。

运行脚本为标准 Refine 应用三件套:devrefine dev)、buildtsc && refine build)、startrefine start),Node 版本要求>=20

2.2 两种启动方式

方式一:使用 create-refine-app 拉取示例

npm create refine-app@latest -- --example form-react-hook-form-use-form

方式二:直接在仓库内运行

cd examples/form-react-hook-form-use-form npm install npm run dev

示例默认通过@refinedev/simple-rest连接 mock REST API,数据资源为postscategories,无需额外配置后端即可体验完整流程。

三、创建页(create.tsx):从零构建 headless 表单

创建页的完整代码位于 examples/form-react-hook-form-use-form/src/pages/posts/create.tsx。其核心骨架如下:

import { useForm } from "@refinedev/react-hook-form"; import { useSelect, useApiUrl, useBack } from "@refinedev/core"; export const PostCreate: React.FC = () => { const { refineCore: { onFinish, formLoading }, register, handleSubmit, formState: { errors }, setValue, } = useForm(); // ... return ( <form onSubmit={handleSubmit(onFinish)}> {/* 字段注册与校验 */} </form> ); };

3.1 关键返回值逐项说明

useForm返回的是 React Hook Form 的UseFormReturn与 Refine 核心useForm返回值的联合增强类型(源码见 packages/react-hook-form/src/useForm/index.ts),除了registerhandleSubmitformStatesetValue等 RHF 原生命令之外,还额外暴露:

返回值来源用途
refineCore.onFinishuseFormCore提交时调用数据提供器的create/update方法
refineCore.formLoadinguseFormCore提交过程中的加载态,用于禁用按钮/展示 Loading
refineCore.queryuseFormCore编辑场景下的数据查询结果
saveButtonProps适配器合成{ disabled, onClick },一键绑定保存按钮

3.2 注册字段与校验

示例使用原生<input>/<select>/<textarea>配合register完成注册与校验规则声明:

<input id="title" {...register("title", { required: true })} /> {errors.title && <span id="title-error">This field is required</span>} <select id="category" defaultValue={""} {...register("category.id", { required: true })}> <option value={""} disabled>Please select</option> {options?.map((category) => ( <option key={category.value} value={category.value}>{category.label}</option> ))} </select> {errors.category && <span id="category-error">This field is required</span>} <textarea id="content" {...register("content", { required: true })} rows={10} cols={50} /> {errors.content && <span id="content-error">This field is required</span>}

这里值得注意的细节:

  • required: true是 React Hook Form 内置校验规则,校验失败时对应errors.<field>会有值,可直接用于条件渲染错误提示;
  • 嵌套字段"category.id"展示了 RHF 的点路径(dot path)能力。表单提交值将产生{ category: { id: 2 } }这样的嵌套结构,与后端数据结构天然对齐;
  • 分类下拉的数据来自useSelect({ resource: "categories", pagination: { mode: "server" } }),这是 Refine 的 headless 选择器,options形如{ value, label }[]

3.3 提交流程:handleSubmit 与 onFinish 的协作

<form onSubmit={handleSubmit(onFinish)}>

handleSubmit来自 React Hook Form:先运行全部校验规则,通过后调用传入的回调。这里的回调直接传 Refine 的onFinish,因此:

  1. RHF 校验所有register字段;
  2. 校验通过后,RHF 将表单值作为参数调用onFinish(values)
  3. onFinish内部根据当前是创建还是编辑(由路由 action 决定)调用数据提供器的createupdate

从源码看,适配器对handleSubmit做了包装(index.ts):在真正提交前调用setWarnWhen(false)清除未保存变更标记,然后转发给 RHF 原生的handleSubmit

3.4 取消与加载态

const back = useBack(); // ... <button onClick={back}>Cancel</button> <input type="submit" disabled={isUploading} value="Submit" /> {formLoading && <p>Loading</p>}

useBack是 Refine 的路由 hook,返回上一页;formLoading在提交期间为true,可据此渲染 Loading 提示。

四、编辑页(edit.tsx):数据回填、懒加载选择器与缩略图

编辑页代码见 examples/form-react-hook-form-use-form/src/pages/posts/edit.tsx。相比创建页,它多出三块核心逻辑。

4.1 自动数据回填

const { refineCore: { onFinish, formLoading, query: queryResult }, register, handleSubmit, formState: { errors }, setValue, saveButtonProps, } = useForm();

refineCore.query是编辑场景下 Refine 自动发起的数据查询(按路由参数中的 id 读取记录)。适配器在查询返回后会把数据自动回填到已注册字段,这正是 headless 场景下「编辑页少写大量setValue」的关键。

4.2 分类下拉:依赖查询结果的懒加载

const { options } = useSelect({ resource: "categories", defaultValue: queryResult?.data?.data?.category?.id, queryOptions: { enabled: !!queryResult?.data?.data?.category?.id, }, pagination: { mode: "server" }, });
  • defaultValue让分类选项在数据到达后默认选中当前记录的分类;
  • queryOptions.enabled控制 categories 查询直到拿到当前记录的 category.id 才发起,避免无意义的提前请求;
  • 另外用useEffect+setValue("category.id", ...)兜底同步,确保 select 组件的受控值在数据到达后正确写入表单(edit.tsx)。

4.3 缩略图展示与saveButtonProps

{queryResult?.data?.data?.thumbnail && ( <img src={queryResult?.data?.data?.thumbnail} width={200} height={200} /> )} <input type="submit" value="Submit" disabled={saveButtonProps.disabled} />

saveButtonProps是适配器额外提供的便捷属性:disabledformLoading时自动为true(源码 index.ts),onClick内部等价于触发handleSubmit(values => onFinish(values).catch(() => {}))。也就是说,你可以直接把它展开到<button {...saveButtonProps}>上,无需手写提交逻辑。

五、文件上传:axios 上传后 setValue 回填

创建页中上传图片的部分展示了 headless 表单「外部异步数据回填表单」的典型写法:

const apiURL = useApiUrl(); const onSubmitFile = async () => { setIsUploading(true); const inputFile = document.getElementById("fileInput") as HTMLInputElement; const formData = new FormData(); formData.append("file", inputFile?.files?.item(0) as File); const res = await axios.post<{ url: string }>( `${apiURL}/media/upload`, formData, { withCredentials: false, headers: { "Access-Control-Allow-Origin": "*" } }, ); setValue("thumbnail", res.data.url); setIsUploading(false); };

要点:

  • useApiUrl()从 Refine 上下文取出数据提供器的 API 地址;
  • 上传成功后用 RHF 的setValue("thumbnail", res.data.url)把返回的 URL 写入隐藏字段:
    <input id="fileInput" type="file" onChange={onSubmitFile} /> <input type="hidden" {...register("thumbnail")} />
  • isUploading控制提交按钮的disabled,防止上传未完成就提交。

这是「先异步处理、再回填表单」的标准模式,编辑页中则直接展示已存在的thumbnail图片,形成创建/编辑闭环。

六、列表页与接口定义

6.1 列表页

list.tsx 使用useTable读取数据并渲染表格,配合useNavigationcreate/edit跳转:

const { tableQuery: tableQueryResult } = useTable<IPost>({ sorters: { initial: [{ field: "id", order: "desc" }] }, }); const { edit, create } = useNavigation(); // <button onClick={() => create("posts")}>Create Post</button> // <button onClick={() => edit("posts", post.id)}>Edit</button>

6.2 接口类型

interfaces/index.d.ts 定义了文章与分类的结构,其中的status联合类型与创建/编辑页的<select>选项一一对应:

export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; }

七、源码深度解析:useForm 适配器如何工作

理解了示例用法后,阅读 packages/react-hook-form/src/useForm/index.ts 可以看清适配器的完整实现,共四层职责。

7.1 双 useForm 组合:RHF 管表单,Core 管数据

适配器内部同时调用两个useForm

const useHookFormResult = useHookForm<TVariables, TContext>({ ...rest }); const useFormCoreResult = useFormCore<...>({ ...refineCoreProps, onMutationError });
  • useHookForm(来自 react-hook-form)负责字段注册、校验、状态管理;
  • useFormCore(来自@refinedev/core)负责数据获取(query)、提交(onFinish)、加载态(formLoading)与自动保存(onFinishAutoSave);
  • 两者的配置通过refineCoreProps与剩余参数(...rest)分别透传,互不干扰。

返回类型UseFormReturnType正是二者返回值的交叉增强(index.ts)。

7.2 配置项与默认值

UseFormProps在 React Hook Form 原生UseHookFormProps基础上扩展了三个 Refine 专属配置(index.ts):

配置项默认值作用
refineCorePropsundefined透传给useFormCore的配置(resourceactionautoSave等)
warnWhenUnsavedChanges继承 Refine 全局配置开启后,表单值变化且未提交时,离开页面会弹出确认框
disableServerSideValidationfalse设为true可关闭服务端校验错误到表单字段的错误映射

其中warnWhenUnsavedChanges的生效路径是:适配器监听watch,任一字段值变化时调用setWarnWhen(true)(index.ts);而handleSubmit提交成功前会setWarnWhen(false)清除标记。这解释了「未保存变更提示」这一完整闭环。

7.3 服务端校验错误自动映射(disableServerSideValidation)

提交失败时,Refine 数据提供器返回的HttpError可能带errors对象。适配器在useFormCoreonMutationError钩子中把服务端字段错误自动映射到对应表单字段(index.ts):

  • 错误值为数组(如["Title is required"])→ 用空格 join 成字符串;
  • 错误值为字符串 → 直接使用;
  • 错误值为true→ 使用兜底文案"Field is not valid."
  • 错误值为{ key, message }对象 → 调用translate(key, message)做 i18n 翻译后展示;
  • 只有已注册到表单的字段(通过flattenObjectKeys(_variables)对比)才会映射,未注册字段会被跳过。

单元测试 packages/react-hook-form/src/useForm/index.spec.tsx 构造了一个包含字符串、数组、true{key,message}四种错误形态的HttpError,逐一验证映射逻辑,同时验证i18nProvidertranslate参与翻译("form.error.content"被译为"Translated content error")。

7.4 查询数据回填:不覆盖用户已编辑内容

编辑页自动回填的核心实现是「延迟同步 + 脏值保护」:

  • 查询数据到达后,通过queueMicrotask(降级为Promise.resolve().then)把同步延迟到字段注册 effect 之后,保证register已挂载的字段能拿到数据(index.ts);
  • syncedFieldsRef记录已同步字段,避免重复setValue覆盖用户输入;
  • 对后期才挂载的字段(如Controller组件),适配器每轮渲染检查control._names.mount集合,发现新字段才做一次尊重脏状态(respectDirty)的同步,即用户已修改的字段不再覆盖(index.ts)。

这套机制保证了「数据加载 → 回填 → 用户编辑 → 再次数据更新」各阶段都不会丢失用户输入。

7.5 自动保存(autoSave)

refineCoreProps.autoSave?.enabledtrue,则onValuesChange会走自动保存分支:调用onFinishAutoSave,并可通过autoSave.onFinish自定义提交前的数据转换(index.ts)。这意味着 useForm 适配器天然支持「输入即保存」的体验,无需额外接入轮询或手动定时器。

八、可复用清单:何时选择 useForm 适配器

结合文档定位与示例实践,@refinedev/react-hook-formuseForm适合以下场景:

  • 项目希望完全掌控 UI(headless),不愿被 antd、MUI 等组件库的表单封装约束;
  • 团队已熟悉 React Hook Form 的register/handleSubmit/formState心智模型;
  • 需要 Refine 的数据层能力(CRUD 提交、数据回填、自动保存、未保存变更提示、服务端错误映射),又希望复用 RHF 强大的校验生态(zodyup等 schema 校验可经 RHF 的resolver接入)。

若需要 Modal 表单或分步表单,同一包还提供了 useModalForm 与 useStepsForm 两个姊妹 hook,均围绕同一套「RHF 表单层 + Refine 数据层」的组合模式实现,可作为后续深入的方向。

九、总结

本文以documentation/docs/examples/form/react-hook-form/useForm.md为入口,围绕 form-react-hook-form-use-form 示例完整走通了 headless 表单的创建、编辑、校验、提交、文件上传与列表跳转流程,并深入 useForm 源码 解析了双 useForm 组合、服务端错误映射、自动回填、未保存变更提示与自动保存的实现原理,最后通过 单元测试 验证了核心行为。掌握这套组合,你就可以在完全自主控制 UI 的前提下,获得 Refine 数据层与 React Hook Form 校验层的双重能力。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 2:45:32

Remotion 客户端渲染报 ‘image is tainted due to CORS‘ 怎么解决?

Remotion 客户端渲染报 image is tainted due to CORS 怎么解决&#xff1f; 【免费下载链接】remotion &#x1f3a5; Make videos programmatically with React 项目地址: https://gitcode.com/GitHub_Trending/re/remotion 在 Remotion 项目使用客户端渲染&#xff0…

作者头像 李华
网站建设 2026/9/12 2:43:47

DevLingo使用指南:程序员专用翻译插件,代码保护与上下文感知

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:43:03

SpringAI Alibaba Graph技术解析与应用实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:41:38

PHP API开发实战:从框架选型到性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:39:56

Android期末大作业:从0到1实现纪念日APP的SQLite存储与倒计时

简介&#xff1a;面向Android初学者与课程设计人群&#xff0c;这份2023年期末大作业「纪念日APP」完整项目包&#xff0c;涵盖了纪念日记录、日历视图、自定义提醒等常用功能&#xff0c;涉及XML布局、Activity跳转、SQLite/SharedPreferences存储、日期计算、通知服务与运行时…

作者头像 李华
网站建设 2026/9/12 2:37:28

t检验实战指南:均值差异分析的核心逻辑与应用要点

t检验这套方法&#xff0c;我在实际项目里用了不下几百次。说实话&#xff0c;很多人一看到“t检验”三个字&#xff0c;第一反应是教材里那个公式&#xff0c;第二反应是SPSS里点两下出个表格。但真正到了实战中——无论是做产品AB实验、医学数据分析&#xff0c;还是用户调研…

作者头像 李华