Refine v5 useDrawerForm 完全指南:在 Drawer 抽屉中实现创建、编辑与自动保存
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文系统讲解 Refine v5 中useDrawerFormHook 的完整用法:它如何把 Ant Design 的<Form>与<Drawer>组合起来,在列表页中实现"点击按钮弹出抽屉 → 填写表单 → 提交并关闭"的经典 CRUD 交互。读完本文,你将掌握 create / edit / clone 三种模式下的抽屉表单写法、syncWithLocation与 URL 同步、autoSave自动保存,以及提交前后数据改写等实战技巧,并能从 源码 层面理解其内部工作原理。
useDrawerForm 是什么
useDrawerForm是@refinedev/antd包提供的 Hook,用于在 Ant Design 的<Drawer>抽屉内管理一个表单。它直接返回 Ant Design<Form>与<Drawer>两个组件所需的 props,你只需要把它们展开到对应组件上即可:
const { formProps, drawerProps, show, saveButtonProps } = useDrawerForm(); return ( <> <Drawer {...drawerProps}> <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> {/* 表单字段 */} </Form> </Create> </Drawer> </> );关键的一点是:useDrawerForm是从useForm扩展而来的。@refinedev/antd中的useForm(其文档见 use-form/index.md)提供的全部能力——如数据获取、mutation、warnWhenUnsavedChanges、mutationMode、overtimeOptions等——在useDrawerForm中都能直接使用。这一点在源码的导出结构中也得到了印证:useDrawerForm内部直接调用了useForm,并把返回值透传出去(见 useDrawerForm.ts)。
从源码类型定义看,useDrawerForm支持四种action:"show" | "edit" | "create" | "clone"(见 useDrawerForm.ts),其中show动作由useShow承担,表单场景最常用的是create与edit。
快速上手:在抽屉中创建记录
以最典型的"文章列表 + 新建文章抽屉"为例。页面主体用useTable渲染列表,useDrawerForm管理抽屉内的创建表单:
import { HttpError } from "@refinedev/core"; import React from "react"; import { Create, List, useDrawerForm, useTable } from "@refinedev/antd"; import { Drawer, Form, Input, Select, Table } from "antd"; interface IPost { id: number; title: string; status: "published" | "draft" | "rejected"; } const PostList: React.FC = () => { const { tableProps } = useTable<IPost, HttpError>(); // highlight-start const { formProps, drawerProps, show, saveButtonProps } = useDrawerForm< IPost, HttpError, IPost >({ action: "create", }); // highlight-end return ( <> <List canCreate // highlight-start createButtonProps={{ onClick: () => { show(); }, }} // highlight-end > <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> </Table> </List> {/* highlight-start */} <Drawer {...drawerProps}> <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Title" name="title" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Status" name="status" rules={[ { required: true, }, ]} > <Select options={[ { label: "Published", value: "published" }, { label: "Draft", value: "draft" }, { label: "Rejected", value: "rejected" }, ]} /> </Form.Item> </Form> </Create> </Drawer> {/* highlight-end */} </> ); };这段代码的要点:
show()打开抽屉。在create模式下不需要传id,因为创建时表单初始是空的。<Create saveButtonProps={saveButtonProps}>把保存按钮与表单提交绑定起来:点击按钮会触发form.submit(),随后执行提交逻辑,成功后抽屉自动关闭、表单自动重置。<Form {...formProps}>负责管理表单状态、初始值、校验规则以及提交动作(onFinish)。
编辑已有记录:show(id) 与手动 EditButton
编辑场景与创建几乎一致,差别在于:打开抽屉时需要把记录id传进去,让表单拉取对应数据并回填;同时用<Edit>组件包裹表单,以支持删除等操作:
import { HttpError } from "@refinedev/core"; import React from "react"; import { Edit, EditButton, List, useDrawerForm, useTable, } from "@refinedev/antd"; import { Drawer, Form, Input, Select, Space, Table } from "antd"; interface IPost { id: number; title: string; status: "published" | "draft" | "rejected"; } const PostList: React.FC = () => { const { tableProps } = useTable<IPost, HttpError>(); // highlight-start const { formProps, drawerProps, show, saveButtonProps, id } = useDrawerForm< IPost, HttpError, IPost >({ action: "edit", warnWhenUnsavedChanges: true, }); // highlight-end return ( <> <List canCreate createButtonProps={{ onClick: () => show() }}> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column<IPost> title="Actions" dataIndex="actions" key="actions" render={(_, record) => ( // highlight-start <Space> <EditButton hideText size="small" recordItemId={record.id} onClick={() => show(record.id)} /> </Space> // highlight-end )} /> </Table> </List> {/* highlight-start */} <Drawer {...drawerProps}> <Edit saveButtonProps={saveButtonProps} recordItemId={id}> <Form {...formProps} layout="vertical"> <Form.Item label="Title" name="title" rules={[{ required: true }]}> <Input /> </Form.Item> <Form.Item label="Status" name="status" rules={[{ required: true }]}> <Select options={[ { label: "Published", value: "published" }, { label: "Draft", value: "draft" }, { label: "Rejected", value: "rejected" }, ]} /> </Form.Item> </Form> </Edit> </Drawer> {/* highlight-end */} </> ); };这里有一个容易忽略的细节:Refine 不会自动为列表中的每条记录添加<EditButton/>,需要手动把它放进操作列。手动放置的目的是让编辑按钮能拿到该行记录的id,并通过show(record.id)通知useDrawerForm拉取数据:
<Table.Column<IPost> title="Actions" dataIndex="actions" key="actions" render={(_value, record) => <EditButton onClick={() => show(record.id)} />} />务必把记录"id"传给show——对于"edit"和"clone"两种模式,没有id就无法获取待编辑数据,抽屉不会正常打开。这一行为在源码中有直接体现:handleShow中,当action为"edit"或"clone"时,只有传入了showId或已存在id才会真正show()(见 useDrawerForm.ts),测试用例也验证了"edit 模式不带 id 调用show()时抽屉保持关闭"(见 index.spec.tsx)。
此外,<Edit recordItemId={id}>中的id来自useDrawerForm的返回值,它记录了当前正在编辑的记录,也是<Edit>头部删除按钮(deleteButtonProps)执行删除操作时的依据。
源码视角:useDrawerForm 内部做了什么
理解返回值之前,先看它的实现骨架(useDrawerForm.ts),这能帮你搞清楚哪些行为是"开箱即用"的:
- 抽屉可见性状态:
useDrawerForm内部使用useDrawer(其实现见 hooks/drawer/useDrawer/index.tsx)管理open状态,初始值由defaultVisible决定(默认false)。 - 表单逻辑全部委托给
useForm:数据获取、提交、mutation、autoSave等均来自useForm的返回值,useDrawerForm只在其外层做抽屉相关的增强。 - 提交后的收尾动作:
useDrawerForm包装了formProps.onFinish——先await onFinish(values)完成真正的提交,然后根据autoSubmitClose(默认true)关闭抽屉,再根据autoResetForm(默认true)调用form.resetFields()清空表单。 drawerProps的固定默认值:width: "500px"、onClose指向内部handleClose、open为当前可见状态、forceRender: true(立即渲染抽屉而非懒渲染)。
这些默认行为在 index.spec.tsx 的测试中都有覆盖:例如"autoSubmitClose为true时提交后抽屉关闭"、"autoSubmitClose为false时提交后抽屉保持打开"、"autoResetForm为true时提交后表单字段被清空"(见 index.spec.tsx)。
Properties:配置项详解
useDrawerForm的 props 完全继承自useForm(完整列表见 use-form/index.md#properties),因此resource、id、redirect、mutationMode、successNotification、errorNotification、meta、queryOptions、warnWhenUnsavedChanges、submitOnEnter、liveMode等全部可用。下面重点展开抽屉表单特有或最常用的几项。
syncWithLocation:抽屉状态与 URL 同步
syncWithLocation默认为false;设为true后,抽屉的可见状态和当前记录的id会同步到 URL 查询参数中。这样刷新页面、前进后退时抽屉状态都能保留,也便于分享带状态的链接。
除了布尔值,它还可以传对象{ key: string; syncId?: boolean }自定义 URL 查询参数的 key:
const drawerForm = useDrawerForm({ syncWithLocation: { key: "my-modal", syncId: true }, });key:自定义查询参数名;syncId:为true时才把id同步进 URL。
如果不自定义key,源码会按drawer-{resource}-{action}的规则自动生成,例如资源posts、动作edit时 key 为drawer-posts-edit(见 useDrawerForm.ts)。同步逻辑通过useGo在路由 query 中写入{ open: true, id },关闭时移除该参数(见 useDrawerForm.ts)。对应的测试会验证:开启syncWithLocation后,getOne请求的meta中会带上"drawer-posts-edit": undefined(见 index.spec.tsx)。
overtimeOptions:请求超时提示
当请求耗时过长、希望展示加载提示时,传入overtimeOptions。interval是轮询间隔(毫秒),onInterval是每个间隔触发的回调。Hook 返回的overtime对象中,elapsedTime表示已经过去的毫秒数,请求完成时变为undefined:
const { overtime } = useDrawerForm({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... // 用法示例:超过 4 秒展示提示 { elapsedTime >= 4000 && <div>this takes a bit longer than expected</div>; }autoSave:自动保存
开启后,用户编辑表单并停止输入一段时间,表单会自动提交保存,无需手动点击保存按钮。
⚠️
autoSave只在edit模式生效。编辑已有数据时改动会自动保存;创建新数据时仍需要手动保存。
enabled
默认false,设为true开启:
useDrawerForm({ action: "edit", autoSave: { enabled: true, }, });debounce
自动保存的防抖时间,默认1000毫秒:
useDrawerForm({ autoSave: { enabled: true, // highlight-next-line debounce: 2000, }, });onFinish
在数据发送到服务器前做改写:
useDrawerForm({ autoSave: { enabled: true, // highlight-start onFinish: (values) => { return { foo: "bar", ...values, }; }, // highlight-end }, });invalidateOnUnmount
Hook 卸载时,失效当前资源关联的list、many、detail查询(可用invalidates属性选择要失效的查询类型),默认false:
useDrawerForm({ autoSave: { enabled: true, // highlight-next-line invalidateOnUnmount: true, }, });invalidateOnClose
抽屉关闭时失效查询,语义与invalidateOnUnmount相同,默认false:
useDrawerForm({ autoSave: { enabled: true, // highlight-next-line invalidateOnClose: true, }, });从源码看,invalidateOnClose的失效逻辑实现在handleClose中:仅当autoSaveProps.status === "success"且配置了invalidateOnClose时,才会调用invalidate({ invalidates: invalidates || ["list", "many", "detail"], ... })(见 useDrawerForm.ts)。
autoSave还支持onMutationSuccess与onMutationError回调,可通过参数isAutoSave判断本次 mutation 是否由自动保存触发(相关行为继承自useForm,详见 use-form/index.md#autosave)。
defaultFormValues:表单默认值
用于预填充表单初始数据:
useDrawerForm({ defaultFormValues: { title: "Hello World", }, });也支持传入异步函数从服务端获取默认值,加载期间可通过返回的defaultFormValuesLoading跟踪状态:
const { defaultFormValuesLoading } = useDrawerForm({ defaultFormValues: async () => { const response = await fetch("https://my-api.com/posts/1"); const data = await response.json(); return data; }, });🚨 当
action为"edit"或"clone"时,异步defaultFormValues可能与记录数据加载产生竞态,此时表单值以最后完成的操作为准。
抽屉专用配置(源码默认值)
除了继承自useForm的 props,useDrawerForm还有三个抽屉相关配置,均定义在 useDrawerForm.ts:
| 配置项 | 默认值 | 说明 |
|---|---|---|
defaultVisible | false | 抽屉初始是否可见 |
autoSubmitClose | true | 提交成功后自动关闭抽屉 |
autoResetForm | true | 提交成功后自动重置表单字段 |
Return Values:返回值详解
useDrawerForm返回useForm的全部返回值(见 use-form/index.md#return-values),外加一组抽屉专用的返回值。
show
打开<Drawer>的函数,接受可选id参数。传入id时会拉取记录数据并回填到<Form>:
show(); // 创建模式:直接打开空表单 show(record.id); // 编辑/克隆模式:携带 id 打开close
关闭<Drawer>的函数,等价于drawerProps.onClose。注意:当warnWhenUnsavedChanges为true时,close内部会先弹出"未保存更改"确认框(源码中通过window.confirm实现,见 useDrawerForm.ts),确认后才真正关闭并清空id。
saveButtonProps
提交按钮所需的 props(disabled、loading、onClick等)。点击时触发form.submit()。源码中的实现为{ disabled: formLoading, onClick: () => form.submit(), loading: formLoading }(见 useDrawerForm.ts)。可以直接传给<Create>、<Edit>的saveButtonProps,也可以手动传给自定义按钮。
deleteButtonProps
删除按钮所需的 props(resource、recordItemId、onSuccess等)。其onSuccess触发时会把id置为undefined并关闭抽屉(见 useDrawerForm.ts)。可手动传给自定义删除按钮。
formProps
管理<Form>状态与动作所必需的 props,底层来自useForm。包含onValuesChange、initialValues、onFieldsChange、onFinish等 Ant Design Form 所需属性(详见 use-form/index.md#formprops)。
:::note 注意onFinish与formProps.onFinish的区别
直接从useDrawerForm返回的onFinish与useForm的onFinish相同;而formProps.onFinish在其基础上做了增强——提交成功后自动关闭抽屉、清空字段。因此,当你想在提交前改写数据时,推荐使用formProps.onFinish并调用它,让它继续接管提交后的收尾操作。
:::
drawerProps
管理<Drawer>状态与动作的 props,包含以下关键项:
| 属性 | 默认值 | 说明 |
|---|---|---|
width | "500px" | 抽屉宽度 |
onClose | 内置handleClose | 关闭抽屉;warnWhenUnsavedChanges为true时先弹出确认框。若自行覆盖该函数,需要手动处理确认框逻辑 |
open | false | 抽屉当前可见状态 |
forceRender | true | 强制渲染抽屉,而非懒渲染 |
overtime
{ elapsedTime?: number },请求超时时间统计,请求完成后elapsedTime变为undefined:
const { overtime } = useDrawerForm(); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ...autoSaveProps
开启autoSave后返回,包含 mutation 的data、error、status属性(status取值为"loading" | "error" | "idle" | "success")。
defaultFormValuesLoading
当defaultFormValues是异步函数时,在函数 resolve 前该值为true。
完整返回值速查表
| Key | 说明 | 类型 |
|---|---|---|
show | 打开抽屉 | (id?: BaseKey) => void |
form | Ant Design 表单实例 | FormInstance<TVariables> |
formProps | 表单 props | FormProps |
drawerProps | 抽屉 props | DrawerProps |
saveButtonProps | 提交按钮 props | { disabled: boolean; onClick: () => void; loading: boolean; } |
deleteButtonProps | 删除按钮 props | { resource?: string; recordItemId?: BaseKey; onSuccess?: (data: TData) => void; mutationMode?: MutationMode; hideText?: boolean; } |
submit | 提交方法 | () => void |
open | 抽屉是否打开 | boolean |
close | 关闭抽屉 | () => void |
overtime | 超时加载状态 | { elapsedTime?: number } |
autoSaveProps | 自动保存状态 | { data?: UpdateResponse<TData>; error: HttpError \| null; status: "loading" \| "error" \| "idle" \| "success" } |
defaultFormValuesLoading | 默认值加载状态 | boolean |
FAQ:提交前如何改写表单数据
有时需要在表单数据发给 API 之前做转换。例如,用户分别输入name和surname两个字段,但 API 期望收到fullName。做法是覆盖<Form>的onFinish,在其中调用formProps.onFinish传入转换后的数据:
import { Create, Drawer, useDrawerForm } from "@refinedev/antd"; import { Form, Input } from "antd"; import React from "react"; export const UserCreate: React.FC = () => { // highlight-start const { formProps, drawerProps, saveButtonProps } = useDrawerForm({ action: "create", }); // highlight-end // highlight-start const handleOnFinish = (values) => { formProps.onFinish?.({ fullName: `${values.name} ${values.surname}`, }); }; // highlight-end return ( <Drawer {...drawerProps}> <Create saveButtonProps={saveButtonProps}> // highlight-next-line <Form {...formProps} onFinish={handleOnFinish} layout="vertical"> <Form.Item label="Name" name="name"> <Input /> </Form.Item> <Form.Item label="Surname" name="surname"> <Input /> </Form.Item> </Form> </Create> </Drawer> ); };这样既完成了数据转换,又保留了"提交成功后自动关闭抽屉、重置表单"的内置行为。
类型参数(Type Parameters)
useDrawerForm是泛型 Hook,与useForm的类型参数一致:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
TQueryFnData | 查询函数返回的数据,继承BaseRecord | BaseRecord | BaseRecord |
TError | 自定义错误对象,继承HttpError | HttpError | HttpError |
TVariables | 表单参数值的类型 | {} | — |
TData | select函数返回的数据,继承BaseRecord;未指定时使用TQueryFnData | BaseRecord | TQueryFnData |
TResponse | mutation 函数返回的数据,继承BaseRecord;未指定时使用TData | BaseRecord | TData |
TResponseError | 自定义错误对象,继承HttpError;未指定时使用TError | HttpError | TError |
在官方示例中查看完整实现
仓库的 examples/form-antd-use-drawer-form 提供了可直接运行的完整示例,其中 list.tsx 同时演示了:
- 一个页面中并存Create 抽屉与Edit 抽屉,两个
useDrawerForm实例都开启了syncWithLocation: true,各自独立与 URL 同步; - 编辑抽屉配合
<Edit recordItemId={id} deleteButtonProps={deleteButtonProps}>实现编辑 + 删除; - 第三个Show 抽屉使用
useShow展示记录详情,用于对比"表单抽屉"与"只读展示抽屉"两种模式。
小结
useDrawerForm的核心价值在于把"表单逻辑 + 抽屉开关 + 提交后收尾"三件事封装在一个 Hook 里:表单能力完全复用useForm,抽屉状态由内部useDrawer管理,提交成功后自动关闭并重置。结合syncWithLocation的状态持久化、autoSave的自动保存与overtimeOptions的加载反馈,你可以在列表页中以极少的样板代码实现完整、顺滑的抽屉式 CRUD 交互。对其内部实现感兴趣的读者,可以继续研读 useDrawerForm.ts 与配套测试 index.spec.tsx,验证本文涉及的每一个默认行为。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考