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)、useSelect、useTable、useApiUrl、useBack、useNavigation等;@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 应用三件套:dev(refine dev)、build(tsc && refine build)、start(refine 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,数据资源为posts与categories,无需额外配置后端即可体验完整流程。
三、创建页(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),除了register、handleSubmit、formState、setValue等 RHF 原生命令之外,还额外暴露:
| 返回值 | 来源 | 用途 |
|---|---|---|
refineCore.onFinish | useFormCore | 提交时调用数据提供器的create/update方法 |
refineCore.formLoading | useFormCore | 提交过程中的加载态,用于禁用按钮/展示 Loading |
refineCore.query | useFormCore | 编辑场景下的数据查询结果 |
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,因此:
- RHF 校验所有
register字段; - 校验通过后,RHF 将表单值作为参数调用
onFinish(values); onFinish内部根据当前是创建还是编辑(由路由 action 决定)调用数据提供器的create或update。
从源码看,适配器对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是适配器额外提供的便捷属性:disabled在formLoading时自动为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读取数据并渲染表格,配合useNavigation的create/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):
| 配置项 | 默认值 | 作用 |
|---|---|---|
refineCoreProps | undefined | 透传给useFormCore的配置(resource、action、autoSave等) |
warnWhenUnsavedChanges | 继承 Refine 全局配置 | 开启后,表单值变化且未提交时,离开页面会弹出确认框 |
disableServerSideValidation | false | 设为true可关闭服务端校验错误到表单字段的错误映射 |
其中warnWhenUnsavedChanges的生效路径是:适配器监听watch,任一字段值变化时调用setWarnWhen(true)(index.ts);而handleSubmit提交成功前会setWarnWhen(false)清除标记。这解释了「未保存变更提示」这一完整闭环。
7.3 服务端校验错误自动映射(disableServerSideValidation)
提交失败时,Refine 数据提供器返回的HttpError可能带errors对象。适配器在useFormCore的onMutationError钩子中把服务端字段错误自动映射到对应表单字段(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,逐一验证映射逻辑,同时验证i18nProvider的translate参与翻译("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?.enabled为true,则onValuesChange会走自动保存分支:调用onFinishAutoSave,并可通过autoSave.onFinish自定义提交前的数据转换(index.ts)。这意味着 useForm 适配器天然支持「输入即保存」的体验,无需额外接入轮询或手动定时器。
八、可复用清单:何时选择 useForm 适配器
结合文档定位与示例实践,@refinedev/react-hook-form的useForm适合以下场景:
- 项目希望完全掌控 UI(headless),不愿被 antd、MUI 等组件库的表单封装约束;
- 团队已熟悉 React Hook Form 的
register/handleSubmit/formState心智模型; - 需要 Refine 的数据层能力(CRUD 提交、数据回填、自动保存、未保存变更提示、服务端错误映射),又希望复用 RHF 强大的校验生态(
zod、yup等 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),仅供参考