Refine useForm 深度解析:面向 create / edit / clone 三种场景的 Headless 表单 Hook
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文以 Refine(refine)官方 3.x 版本文档useForm的 API Reference 为主体,系统讲解这一 Headless 表单 Hook 的三种 action 模式、完整数据流、全部配置参数与返回值,并结合当前仓库 packages/core/src/hooks/form/index.ts 中的最新源码实现,剖析useForm在底层如何编排useOne、useCreate、useUpdate完成表单提交。读完本文,你将能够独立搭建基于useForm的创建/编辑/克隆表单页面,理解其重定向、缓存失效、变更模式(mutation mode)与通知机制,并掌握自动保存(autoSave)等仓库中已落地的进阶用法。
useForm 的定位:连接状态与 dataProvider 的桥梁
useForm是 Refine 中用于管理表单的 Hook,它内置了处理create、edit和clone三种action的能力。Hook 的返回值与调用时对应的action相匹配,并会依据不同的action执行不同的逻辑。
可以把useForm理解为你的表单state与dataProvider之间的"桥梁"。它是一个低层级的 Hook,专门用于构建你自己的表单组件;同时,它还会借助notificationProvider根据当前action和dataProvider的响应结果向用户反馈通知。
需要注意的一点(原文档明确强调):useForm本身不管理任何表单状态。如果你需要的是完整的表单库,Refine 官方支持三种开箱即用的表单库:
- React Hook Form(面向 Headless 用户);
- Ant Design Form(面向 Ant Design 用户);
- Mantine Form(面向 Mantine 用户)。
这三类库在 Refine 中同样提供了各自的useForm封装,与本文讲解的 core 版本useForm是不同层次的抽象,使用时不要混淆。
三种 action 的底层工作流程
useForm的行为围绕action展开,默认行为可以分为三个分支:
action: "create"
表单提交后:
useForm调用onFinish函数,传入表单值;onFinish内部调用 useCreate Hook,并传入表单值;useCreate调用dataProvider的create方法并返回响应;- 根据响应状态,
useForm调用onSuccess或onError; onSuccess/onError进而调用notificationProvider的open方法通知用户;useForm重定向到list页面。
action: "edit"
挂载时,useForm先调用 useOne Hook 获取待编辑的记录,其中id来自URL或 props。表单提交后:
useForm调用onFinish,传入表单值;onFinish内部调用 useUpdate;useUpdate调用dataProvider的update方法并返回响应;- 根据响应状态调用
onSuccess或onError; - 进而调用
notificationProvider的open通知用户; - 重定向到
list页面。
action: "clone"
挂载时与edit类似,useForm调用useOne获取待克隆的记录(id来自URL或 props)。可以把它理解为"另存为(save as)":与edit相同的是先读取记录,不同的是提交时创建一条新记录而非更新原记录——onFinish内部调用useCreate,随后同样经过通知与重定向到list页面。
以上是默认行为。你可以通过传入redirect、onMutationSuccess、onMutationError等 props 来自定义它。
基础用法
下面是一个最小可运行的创建表单(继承自原文档 Basic Usage 示例):
import { useState } from "react"; import { useForm } from "@pankod/refine-core"; const PostCreate = () => { const [title, setTitle] = useState(); const { onFinish } = useForm({ action: "create", }); const onSubmit = (e) => { e.preventDefault(); onFinish({ title }); }; return ( <form onSubmit={onSubmit}> <input onChange={(e) => setTitle(e.target.value)} /> <button type="submit">Submit</button> </form> ); };- 调用
onFinish后会返回mutationResult; useForm接受泛型类型参数,用于定义 mutation 与 query 的响应类型。
完整配置参数详解(Properties)
action
useForm支持edit、create、clone三种 action。
默认行为:从路由推断
action。
- 路由为
/posts/create时,Hook 以action: "create"运行;- 路由为
/posts/edit/1时,以action: "edit"运行;- 路由为
/posts/clone/1时,以action: "clone"运行。
当无法从路由推断 action 时(例如表单放在弹窗中、或使用了自定义路由),可以显式传入actionprop 覆盖。
action: "create":用于创建一条此前不存在的记录,底层使用useCreate执行 mutation;action: "edit":用于编辑已有记录,需要id。默认使用路由中的id,可通过setId函数或id属性修改;挂载时用useOne按id拉取记录并通过queryResult返回,供你填充表单,提交后调用useUpdate;action: "clone":用于克隆已有记录。同样需要id,可用setId修改;用useOne拉取记录填充表单,提交时通过useCreate创建新记录。
resource
默认值:从当前 URL 读取
resource。
该值会作为参数传给dataProvider的各个方法,通常被用作 API 端点路径,具体取决于你的dataProvider如何处理resource。按 action 不同:
action: "create"时,传给dataProvider的create方法;action: "edit"时,传给update和getOne方法;action: "clone"时,传给create和getOne方法。
useForm({ resource: "categories", });id
id用于确定要edit或clone的记录。默认使用路由中的id,也可以通过setId函数或id属性修改。当你要在非标准页面(例如详情页)编辑/克隆某个资源时非常有用。
注意:
action: "edit"或action: "clone"时id是必需的。
useForm({ action: "edit", // or clone resource: "categories", id: 1, // <BASE_URL_FROM_DATA_PROVIDER>/categories/1 });redirect
用于指定表单提交成功后的重定向页面,默认为list。可设置为"show" | "edit" | "list" | "create",或设为false以阻止提交后自动跳转。
useForm({ redirect: false, });onMutationSuccess
mutation 成功后的回调,接收三个参数:
data:根据action不同,来自useCreate或useUpdate的返回数据;variables:传给 mutation 的变量;context:react-query 的上下文。
useForm({ onMutationSuccess: (data, variables, context) => { console.log({ data, variables, context }); }, });onMutationError
mutation 失败后的回调,参数结构同上(data、variables、context):
useForm({ onMutationError: (data, variables, context) => { console.log({ data, variables, context }); }, });invalidates
用于管理 mutation 结束后触发的缓存失效(invalidation)。针对当前resource的默认失效范围是:
"create"或"clone"模式:失效"list"和"many";"edit"模式:失效"list"、"many"和"detail"。
useForm({ invalidates: ["list", "many", "detail"], });dataProviderName
当你注册了多个dataProvider时,需要指明当前表单使用哪一个。它适合为特定资源切换dataProvider。
提示:如果希望在所有资源页面统一使用不同的
dataProvider,可以直接使用<Refine>组件的dataProvider属性,无需在useForm里逐个指定。
useForm({ dataProviderName: "second-data-provider", });mutationMode
变更模式决定 mutation 以何种方式执行,共有三种:pessimistic、optimistic和undoable,默认是pessimistic。每种模式对应不同的用户体验:
pessimistic:先请求、成功后更新 UI(传统做法);optimistic:先更新 UI,请求失败再回滚;undoable:先更新 UI,在超时窗口内提供"撤销"入口。
useForm({ mutationMode: "undoable", // "pessimistic" | "optimistic" | "undoable" });successNotification
前提:需要配置
NotificationProvider才生效。
表单提交成功后,useForm会调用NotificationProvider的open方法展示成功通知。该属性用于自定义成功通知的内容:
useForm({ successNotification: (data, values, resource) => { return { message: `Post Successfully created with ${data.title}`, description: "Success with no errors", type: "success", }; }, });errorNotification
同样依赖NotificationProvider。表单提交失败后,useForm调用open展示错误通知,可用该属性自定义。不传时的默认值如下:
{ "message": "Error when updating <resource-name> (status code: ${err.statusCode}) 或 Error when creating <resource-name> (status code: ${err.statusCode})", "description": "Error", "type": "error" }useForm({ errorNotification: (data, values, resource) => { return { message: `Something went wrong when deleting ${data.id}`, description: "Error", type: "error", }; }, });metaData
metaData有两个用途:
- 向 data provider 方法传递额外信息;
- 使用普通 JavaScript 对象(JSON)生成 GraphQL 查询(用于 GraphQL dataProvider 场景)。
例如,把headers放进metaData传给create方法;类似的逻辑可以让你向 data provider 传递任意自定义属性:
useForm({ metaData: { headers: { "x-meta-data": "true" }, }, }); const myDataProvider = { //... create: async ({ resource, variables, metaData }) => { const headers = metaData?.headers ?? {}; const url = `${apiUrl}/${resource}`; const { data } = await httpClient.post(url, variables, { headers }); return { data, }; }, //... };queryOptions
仅在
action: "edit"或action: "clone"模式下生效。
edit/clone模式下 Refine 使用useOne拉取数据,可以通过queryOptions传入 react-query 的useQuery选项:
useForm({ queryOptions: { retry: 3, }, });createMutationOptions
仅在
action: "create"或action: "clone"时可用。
create/clone模式下 Refine 用useCreate创建数据,可通过createMutationOptions传入 react-query 的useMutation选项:
useForm({ createMutationOptions: { retry: 3, }, });updateMutationOptions
仅在
action: "edit"时可用。
edit模式下 Refine 用useUpdate更新数据,可通过updateMutationOptions传入useMutation选项:
useForm({ updateMutationOptions: { retry: 3, }, });liveMode / onLiveEvent / liveParams
实时(Live / Realtime)相关配置,配合liveProvider使用:
liveMode:接收到相关 live 事件时是否自动更新数据("auto")或不自动更新("manual"),用于在全应用范围内实时更新并展示数据;onLiveEvent:订阅到达新事件时执行的回调;liveParams:传给liveProvider的subscribe方法的参数。
useForm({ liveMode: "auto", onLiveEvent: (event) => { console.log(event); }, });返回值(Return Values)
| 属性 | 说明 | 类型 |
|---|---|---|
onFinish | 触发 mutation 的函数 | (values: TVariables) => Promise<CreateResponse<TData> \| UpdateResponse<TData> \| void> |
queryResult | 记录查询(query)的结果 | QueryObserverResult<T> |
mutationResult | 调用onFinish后触发的 mutation 结果 | UseMutationResult<T> |
formLoading | 表单请求的加载状态 | boolean |
id | clone/edit场景下的记录 id | BaseKey |
setId | id的 setter | Dispatch<SetStateAction<string \| number \| undefined>> |
redirect | 自定义重定向函数 | (redirect: "list"\|"edit"\|"show"\|"create"\|false, idFromFunction?: BaseKey) => void |
queryResult
当action为"edit"或"clone",或者提供了带id的resource时,useForm会调用useOne,并把结果作为queryResult属性返回:
const { queryResult } = useForm(); const { data } = queryResult;mutationResult
"create"/"clone"模式下useForm调用useCreate,"edit"模式下调用useUpdate,并把结果作为mutationResult返回:
const { mutationResult } = useForm(); const { data } = mutationResult;setId
useForm默认从 router 推断id。若想动态修改id,可以使用setId:
const { id, setId } = useForm(); const handleIdChange = (id: string) => { setId(id); }; return ( <div> <input value={id} onChange={(e) => handleIdChange(e.target.value)} /> </div> );redirect
默认情况下,mutation 成功后useForm会重定向到"list"页面。要跳到其他页面,可以用返回的redirect函数在代码中指定目标,或在 hook 选项中设置redirect属性。例如成功提交后跳转到"show"页面:
const { onFinish, redirect } = useForm(); // -- const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const data = await onFinish(formValues); redirect("show", data?.data?.id); }; // --onFinish
onFinish在表单提交时被调用,会根据action选择对应的 mutation。你也可以在 hook 选项中传入自定义的onFinish函数来覆盖默认行为,例如在提交前修改表单数据(见下文 FAQ)。
formLoading
表单请求的加载状态:当useForm正在提交,或正在为"edit"/"clone"模式拉取数据时为true。适合直接绑定到提交按钮的disabled上。
源码级实现剖析(结合当前仓库)
以下分析基于当前仓库中 useForm 的实现文件。该文件是 3.x 文档所述实现的直接演进(@refinedev/corev5),核心编排逻辑与文档描述一致,同时扩展了 autoSave、overtime 等能力。
参数解析:从路由推断 action 与 id
实现开头通过useResourceParams一次性解析出id、resource、formAction:
const { id, setId, resource, identifier, formAction: action, } = useResourceParams({ resource: props.resource, id: props.id, action: props.action, }); const isEdit = action === "edit"; const isClone = action === "clone"; const isCreate = action === "create";随后基于三个布尔值决定后续所有行为分支。若edit/clone场景未定义id(且未通过queryOptions.enabled: false禁用查询),会用warnOnce输出开发态警告,提示应显式传入idprop 或调用setId——这正对应文档中"id在 edit/clone 时必需"的约束。
重定向策略的解析
重定向目标由redirectPage辅助函数统一计算,优先级是 props 中的redirect> 全局默认配置(来自useRefineOptions)> 按 action 推导的默认值:
const redirectAction = redirectPage({ redirectFromProps: props.redirect, action, redirectOptions: defaultRedirect, });返回给用户的redirect函数内部调用handleSubmitWithRedirect(来自useRedirectionAfterSubmission),支持第二个参数传入目标 id、第三个参数传入 route params:
const redirect: UseFormReturnType["redirect"] = ( redirect = isEdit ? "list" : "edit", redirectId = id, routeParams = {}, ) => { handleSubmitWithRedirect({ redirect: redirect, resource, id: redirectId, meta: { ...pickedMeta, ...routeParams }, }); };查询与 mutation 的编排
Hook 同时挂载了三个数据 Hook,并依据action选用其一:
const queryResult = useOne<TQueryFnData, TError, TData>({ resource: identifier, id, queryOptions: { ...props.queryOptions, // 只有非 create 且 id 已定义时才启用查询 enabled: !isCreate && id !== undefined && (props.queryOptions?.enabled ?? true), }, liveMode: props.liveMode, onLiveEvent: props.onLiveEvent, liveParams: props.liveParams, meta: { ...combinedMeta, ...props.queryMeta }, dataProviderName: props.dataProviderName, }); const createMutation = useCreate<TResponse, TResponseError, TVariables>({ mutationOptions: props.createMutationOptions, }); const updateMutation = useUpdate<TResponse, TResponseError, TVariables>({ mutationOptions: props.updateMutationOptions, }); const mutationResult = isEdit ? updateMutation : createMutation; const isMutationLoading = mutationResult.mutation.isPending; const formLoading = isMutationLoading || queryResult.query.isFetching;这里可以看到文档中"queryOptions 仅在 edit/clone 生效"的源码依据:useOne的enabled被强制与!isCreate && id !== undefined做与运算,即使你显式传了enabled,create 模式下查询也不会发起。formLoading则是 mutation 的 pending 状态与查询的 isFetching 状态的并集,即"提交中或拉取编辑/克隆数据中"。
onFinish:一次提交如何走完全流程
onFinish是整个 Hook 的核心(源码约 L214–L303)。它返回一个 Promise,内部流程完整对应文档描述的工作流:
- 前置校验,缺少必要参数时直接 reject,并带有明确的错误文案:
// Reject the mutation if the resource is not defined if (!resource) return reject(missingResourceError); // Reject the mutation if the `id` is not defined in clone action if (isClone && !id) return reject(missingIdError); // Reject the mutation if there's no `values` passed if (!values) return reject(missingValuesError); // Auto Save is only allowed in edit action if (isAutosave && !isEdit) return reject(autosaveOnNonEditError);- 按 mutationMode 决定重定向时机:非悲观模式(optimistic/undoable)下,重定向被
deferExecution延迟执行(先提交 UI 状态再跳转),Promise 立即 resolve;悲观模式则在 mutation 成功后、.then分支中延迟重定向到目标页面:
mutateAsync(variables as any, { onSuccess: props.onMutationSuccess ? (data, _, context) => { props.onMutationSuccess?.(data, values, context, isAutosave); } : undefined, onError: props.onMutationError ? (error: TResponseError, _, context) => { props.onMutationError?.(error, values, context, isAutosave); } : undefined, }) .then((data) => { if (isPessimistic && !isAutosave) { deferExecution(() => onSuccessRedirect(data?.data?.id)); } ... resolve(data); }) .catch(reject);- 透传变量:
variables中组装了values、resource、合并后的 meta(meta与mutationMeta)、dataProviderName、invalidates,以及 edit 模式专属的id、mutationMode、undoableTimeout、optimisticUpdateMap——这正是文档中invalidates、mutationMode、metaData各属性能够生效的落点。successNotification/errorNotification也在此处一并传给 mutation,由下游 Hook 在响应到达后调用notificationProvider。
类型定义与文档参数表的对应
useForm 的类型定义文件 中,每个文档参数的注释与默认值都有精确对应:
action:@default Action that it reads from route otherwise "create" is used;resource:@default Resource name that it reads from route;id:@default Id that it reads from the URL;redirect:@default "list",类型为"show" | "edit" | "list" | "create" | false;mutationMode:@default "pessimistic"*(带*号表示该默认值来自RefineContext,即可以在<Refine>组件上设置全局默认值,本地传入值优先);undoableTimeout:@default 5000*(undoable 模式下撤销窗口的等待时长);invalidates:@default ["list", "many", "detail"],可选值包括all、resourceAll、list、many、detail、false。
说明:3.x 文档中的
metaData属性在当前仓库源码中已演进为meta/queryMeta/mutationMeta三段式命名,分别作用于全局 meta 合并、useOne查询与 mutation,语义与原文档一致。
测试用例对文档行为的验证
useForm 的测试文件 对文档描述的关键行为做了逐条验证,例如:
- "fetches data when in edit mode":edit 路由下挂载
useForm,等待formLoading变为 false 后断言query.data.data.title等于 mock 的 posts 首条记录——验证了"挂载时用 useOne 拉取记录填充表单"; - "correctly reads id value from route":断言
result.current.id等于路由中的"1"——验证了 id 的路由推断; - "uses the correct meta values when fetching data":通过 mock
getOne断言meta合并结果同时包含meta与queryMeta的键——验证了 meta 合并逻辑。
这些测试均运行在MockJSONServer+ mock routerProvider 之上,与文档中"dataProvider 的 create/update/getOne 被按 action 调用"的描述一致。
FAQ
如何失效其他资源(Invalidate other resources)?
invalidates只作用于当前resource。若要失效与当前资源无直接关系的其他资源,可以配合useInvalidateHook,在onMutationSuccess中调用:
import { useInvalidate, useForm } from "@pankod/refine-core"; const PostEdit = () => { const invalidate = useInvalidate(); useForm({ onMutationSuccess: (data, variables, context) => { invalidate({ resource: "users", invalidates: ["resourceAll"], }); }, }); // --- };如何在提交到 API 之前修改表单数据?
有时需要把用户在界面上分开填写的值合并后再发送。例如将name与surname两个输入合并为fullName:
import React, { useState } from "react"; import { useForm } from "@pankod/refine-core"; export const UserCreate: React.FC = () => { const [name, setName] = useState(); const [surname, setSurname] = useState(); const { onFinish } = useForm(); const onSubmit = (e) => { e.preventDefault(); const fullName = `${name} ${surname}`; onFinish({ fullName: fullName, name, surname, }); }; return ( <form onSubmit={onSubmit}> <input onChange={(e) => setName(e.target.value)} /> <input onChange={(e) => setSurname(e.target.value)} /> <button type="submit">Submit</button> </form> ); };官方示例与进阶用法:autoSave
仓库内置的 form-core-use-form 示例 是本文用法的完整落地。其创建页 create.tsx 展示了文档中useForm<IPost, HttpError, FormValues>泛型用法的一个实用细节:同一个组件同时处理 create 与 clone,当 action 为 clone 时query返回值非空,用它初始化defaultValue:
export const PostCreate: React.FC = () => { const { formLoading, onFinish, query: queryResult } = useForm<IPost, HttpError, FormValues>(); // if action is "clone", we'll have defaultValues const defaultValues = queryResult?.data?.data; const submit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const formData = new FormData(event.currentTarget); onFinish({ title: formData.get("title") as string, content: formData.get("content") as string, }).catch(() => {}); }; // ... };其编辑页 edit.tsx 则演示了 3.x 文档之后仓库新增的autoSave(自动保存)能力:
export const PostEdit: React.FC = () => { const { formLoading, onFinish, query: queryResult, autoSaveProps, onFinishAutoSave, } = useForm<FormValues>({ autoSave: { enabled: true, }, }); // ... return ( <div> <AutoSaveIndicator {...autoSaveProps} /> <form onSubmit={(event) => submit(event)} onChange={(event) => submit(event, true)} > {/* 表单字段 ... */} </form> </div> ); };从源码看,onFinishAutoSave是经过asyncDebounce(默认 1000ms)防抖包装的onFinish,且强制附带isAutosave: true;autoSave 的 mutation 会自动关闭 notification 与缓存失效(invalidates: []),避免每次输入都弹通知或刷缓存;配合autoSave.invalidateOnUnmount,组件卸载时会统一触发一次失效。AutoSaveIndicator组件则基于autoSaveProps(status/data/error)展示"已保存/失败/保存中"的指示器。
泛型参数说明(Type Parameters)
| 参数 | 描述 | 默认值 |
|---|---|---|
TData | 查询结果数据类型,扩展BaseRecord | BaseRecord |
TError | 自定义错误对象,扩展HttpError | HttpError |
TVariables | 提交参数的值类型 | {} |
注意带*标记的属性(如mutationMode、undoableTimeout)默认值来自RefineContext,即可以在<Refine>组件上设置全局默认;useForm本地传入的值会覆盖全局值。
版本说明与适用前提
本文正文以 version-3.xx.xx 文档 为准,该版本对应@pankod/refine-core包名与 react-query v4 时代的 API(返回queryResult/mutationResult属性)。当前仓库主干为@refinedev/corev5(基于 TanStack Query v5),源码中的实现对本文内容有如下可直接确认的演进:
- 返回值由
queryResult/mutationResult重命名为query/mutation(官方示例中已使用新命名); - 新增
autoSave(含enabled、debounce、invalidateOnUnmount、invalidateOnClose子选项)与overtime超时计时能力; metaData演进为meta/queryMeta/mutationMeta三段式;- 新增 4 个泛型参数(
TData、TResponse等)以覆盖 select 与响应类型的映射。
如果你正在按 3.x 文档迁移到新版,建议以 当前源码类型定义 中的 JSDoc 注释作为参数默认值的最终依据。
小结
useForm是 Refine 数据层表单能力的编排者:它不碰表单状态,而是把"路由推断 → 按 action 选择 useOne/useCreate/useUpdate → mutation → 通知 → 重定向 → 缓存失效"这条链路封装成一个 Hook。理解它的三种 action 工作流、redirect/invalidates/mutationMode三类控制参数,以及onFinish返回 Promise 的语义,就能在 Headless 场景下自由组合任意 UI 框架搭建表单页面;而仓库中的 form 源码、测试用例 与 form-core-use-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),仅供参考