news 2026/9/12 11:45:53

Refine v5 useDrawerForm 完全指南:在 Drawer 抽屉中实现创建、编辑与自动保存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 useDrawerForm 完全指南:在 Drawer 抽屉中实现创建、编辑与自动保存

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、warnWhenUnsavedChangesmutationModeovertimeOptions等——在useDrawerForm中都能直接使用。这一点在源码的导出结构中也得到了印证:useDrawerForm内部直接调用了useForm,并把返回值透传出去(见 useDrawerForm.ts)。

从源码类型定义看,useDrawerForm支持四种action"show" | "edit" | "create" | "clone"(见 useDrawerForm.ts),其中show动作由useShow承担,表单场景最常用的是createedit

快速上手:在抽屉中创建记录

以最典型的"文章列表 + 新建文章抽屉"为例。页面主体用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),这能帮你搞清楚哪些行为是"开箱即用"的:

  1. 抽屉可见性状态useDrawerForm内部使用useDrawer(其实现见 hooks/drawer/useDrawer/index.tsx)管理open状态,初始值由defaultVisible决定(默认false)。
  2. 表单逻辑全部委托给useForm:数据获取、提交、mutation、autoSave等均来自useForm的返回值,useDrawerForm只在其外层做抽屉相关的增强。
  3. 提交后的收尾动作useDrawerForm包装了formProps.onFinish——先await onFinish(values)完成真正的提交,然后根据autoSubmitClose(默认true)关闭抽屉,再根据autoResetForm(默认true)调用form.resetFields()清空表单。
  4. drawerProps的固定默认值width: "500px"onClose指向内部handleCloseopen为当前可见状态、forceRender: true(立即渲染抽屉而非懒渲染)。

这些默认行为在 index.spec.tsx 的测试中都有覆盖:例如"autoSubmitClosetrue时提交后抽屉关闭"、"autoSubmitClosefalse时提交后抽屉保持打开"、"autoResetFormtrue时提交后表单字段被清空"(见 index.spec.tsx)。

Properties:配置项详解

useDrawerForm的 props 完全继承自useForm(完整列表见 use-form/index.md#properties),因此resourceidredirectmutationModesuccessNotificationerrorNotificationmetaqueryOptionswarnWhenUnsavedChangessubmitOnEnterliveMode等全部可用。下面重点展开抽屉表单特有或最常用的几项。

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:请求超时提示

当请求耗时过长、希望展示加载提示时,传入overtimeOptionsinterval是轮询间隔(毫秒),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 卸载时,失效当前资源关联的listmanydetail查询(可用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还支持onMutationSuccessonMutationError回调,可通过参数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:

配置项默认值说明
defaultVisiblefalse抽屉初始是否可见
autoSubmitClosetrue提交成功后自动关闭抽屉
autoResetFormtrue提交成功后自动重置表单字段

Return Values:返回值详解

useDrawerForm返回useForm的全部返回值(见 use-form/index.md#return-values),外加一组抽屉专用的返回值。

show

打开<Drawer>的函数,接受可选id参数。传入id时会拉取记录数据并回填到<Form>

show(); // 创建模式:直接打开空表单 show(record.id); // 编辑/克隆模式:携带 id 打开

close

关闭<Drawer>的函数,等价于drawerProps.onClose。注意:当warnWhenUnsavedChangestrue时,close内部会先弹出"未保存更改"确认框(源码中通过window.confirm实现,见 useDrawerForm.ts),确认后才真正关闭并清空id

saveButtonProps

提交按钮所需的 props(disabledloadingonClick等)。点击时触发form.submit()。源码中的实现为{ disabled: formLoading, onClick: () => form.submit(), loading: formLoading }(见 useDrawerForm.ts)。可以直接传给<Create><Edit>saveButtonProps,也可以手动传给自定义按钮。

deleteButtonProps

删除按钮所需的 props(resourcerecordItemIdonSuccess等)。其onSuccess触发时会把id置为undefined并关闭抽屉(见 useDrawerForm.ts)。可手动传给自定义删除按钮。

formProps

管理<Form>状态与动作所必需的 props,底层来自useForm。包含onValuesChangeinitialValuesonFieldsChangeonFinish等 Ant Design Form 所需属性(详见 use-form/index.md#formprops)。

:::note 注意onFinishformProps.onFinish的区别

直接从useDrawerForm返回的onFinishuseFormonFinish相同;而formProps.onFinish在其基础上做了增强——提交成功后自动关闭抽屉、清空字段。因此,当你想在提交前改写数据时,推荐使用formProps.onFinish并调用它,让它继续接管提交后的收尾操作。

:::

drawerProps

管理<Drawer>状态与动作的 props,包含以下关键项:

属性默认值说明
width"500px"抽屉宽度
onClose内置handleClose关闭抽屉;warnWhenUnsavedChangestrue时先弹出确认框。若自行覆盖该函数,需要手动处理确认框逻辑
openfalse抽屉当前可见状态
forceRendertrue强制渲染抽屉,而非懒渲染

overtime

{ elapsedTime?: number },请求超时时间统计,请求完成后elapsedTime变为undefined

const { overtime } = useDrawerForm(); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ...

autoSaveProps

开启autoSave后返回,包含 mutation 的dataerrorstatus属性(status取值为"loading" | "error" | "idle" | "success")。

defaultFormValuesLoading

defaultFormValues是异步函数时,在函数 resolve 前该值为true

完整返回值速查表

Key说明类型
show打开抽屉(id?: BaseKey) => void
formAnt Design 表单实例FormInstance<TVariables>
formProps表单 propsFormProps
drawerProps抽屉 propsDrawerProps
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 之前做转换。例如,用户分别输入namesurname两个字段,但 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查询函数返回的数据,继承BaseRecordBaseRecordBaseRecord
TError自定义错误对象,继承HttpErrorHttpErrorHttpError
TVariables表单参数值的类型{}
TDataselect函数返回的数据,继承BaseRecord;未指定时使用TQueryFnDataBaseRecordTQueryFnData
TResponsemutation 函数返回的数据,继承BaseRecord;未指定时使用TDataBaseRecordTData
TResponseError自定义错误对象,继承HttpError;未指定时使用TErrorHttpErrorTError

在官方示例中查看完整实现

仓库的 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),仅供参考

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

SqlSugar ORM框架核心特性与高级应用实战

/* 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 11:45:07

电子元器件智能识别:YOLO系列与大模型深度融合实战解析

/* 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 11:42:16

嵌入式软件工程师面试高频考点与项目实战复盘指南

这几年我陆陆续续参加了不少嵌入式岗位的面试&#xff0c;也帮团队做过技术面&#xff0c;慢慢摸清了这套东西的考察逻辑。嵌入式面试和互联网后台开发面试完全是两个路子&#xff0c;它不问高并发、不问分布式&#xff0c;问的是指针怎么用、内存怎么分配、中断里能不能做延时…

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

STM32F103驱动VS1053音乐播放器:SPI时序与无声排错全解析

简介&#xff1a;一套基于STM32F103标准库开发的VS1053音乐播放器完整工程&#xff0c;面向单片机初学者及需要快速嵌入音频播放功能的开发者。压缩包共176个文件&#xff0c;以72个C源文件、71个头文件为主&#xff0c;另含若干PNG图片、TXT说明及工程配置文件&#xff0c;整体…

作者头像 李华
网站建设 2026/9/12 11:40:05

10分钟跑通CUDA程序:AMD显卡ZLUDA配置完整指南

10分钟跑通CUDA程序&#xff1a;AMD显卡ZLUDA配置完整指南 【免费下载链接】ZLUDA CUDA on non-NVIDIA GPUs 项目地址: https://gitcode.com/GitHub_Trending/zl/ZLUDA CUDA程序只能在N卡上跑&#xff0c;AMD卡干看着&#xff1f;ZLUDA 是一套“即插即用”的 CUDA 运行时…

作者头像 李华