Refine v5 实战:使用 useModalForm 在 Ant Design Modal 中构建创建、编辑与克隆表单
2026/9/12 9:18:08 网站建设 项目流程

Refine v5 实战:使用 useModalForm 在 Ant Design Modal 中构建创建、编辑与克隆表单

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

useModalForm是 Refine v5 中@refinedev/antd包提供的一个表单 Hook,用于在 Ant Design 的<Modal>弹窗组件内完整地创建、编辑和克隆记录。本文以仓库中的示例 form-antd-use-modal-form 为主体,结合 官方 useModalForm 参考文档 与 源码实现,系统讲解其用法、全部配置项、返回值与底层原理,帮助你在一页列表内用弹窗表单完成整套 CRUD 交互。

认识 useModalForm

useModalForm允许你在<Modal>组件内部管理一个表单,并返回 Ant Design<Form><Modal>所需的全部 props。你只需要把modalProps展开到<Modal>、把formProps展开到<Form>,弹窗的开关、表单的提交、数据回填、提交后关闭与重置等行为即可开箱即用。

关键的一点是:useModalForm是从@refinedev/antd包中的useForm扩展而来的(源码中通过import { useForm, type UseFormProps, type UseFormReturnType } from "../useForm";组合实现),这意味着useForm的全部能力——数据加载、提交、表单校验、warnWhenUnsavedChangesovertimeOptionsautoSave等——都可以直接在useModalForm中使用。

从 useModalForm.ts 的类型定义可以看到,UseModalFormPropsUseFormPropsCoreUseFormPropsuseModalFormConfigLiveModePropsFormWithSyncWithLocationParams组合而来,并额外增加了四个 Modal 专属配置:

export type UseModalFormProps<...> = UseFormPropsCore<...> & UseFormProps<...> & useModalFormConfig & LiveModeProps & FormWithSyncWithLocationParams & { defaultVisible?: boolean; // 默认是否可见,默认 false autoSubmitClose?: boolean; // 提交成功后自动关闭,默认 true autoResetForm?: boolean; // 提交成功后重置表单,默认 true autoResetFormWhenClose?: boolean; // 关闭时重置表单,默认 true };

useModalFormConfig限定了action的取值:"show" | "edit" | "create" | "clone",这是驱动整个 Hook 行为的分支开关。

快速开始:在列表页中实现"创建"弹窗

我们先看最基础的创建场景。完整示例位于 examples/form-antd-use-modal-form/src/pages/posts/list.tsx,其核心思路是:用useTable渲染列表,用action: "create"useModalForm管理新增弹窗,点击<List>自带的创建按钮时调用show()打开弹窗。

import React from "react"; import { List, useModalForm, useTable } from "@refinedev/antd"; import { Form, Input, Modal, Select, Table } from "antd"; const PostList: React.FC = () => { const { tableProps } = useTable<IPost>(); // 创建弹窗:action 指定为 "create" const { modalProps: createModalProps, formProps: createFormProps, show: createModalShow, } = useModalForm<IPost>({ action: "create", }); return ( <> <List // createButtonProps 用于配置列表上方的新建按钮, // 点击后通过 createModalShow() 打开弹窗 createButtonProps={{ onClick: () => { createModalShow(); }, }} > <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column dataIndex="status" title="Status" /> </Table> </List> <Modal {...createModalProps}> <Form {...createFormProps} 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> </Modal> </> ); }; interface IPost { id: number; title: string; status: "published" | "draft" | "rejected"; }

action: "create"时,show()无需传id;提交时useModalForm会自动调用 data provider 的create方法创建记录,并在成功后根据autoSubmitCloseautoResetForm的默认行为自动关闭弹窗、清空表单。

编辑弹窗:通过 record id 回填数据

action: "edit"的用法与创建几乎一致,区别在于:必须把当前记录的id传给show(record.id)useModalForm才会据此调用getOne拉取数据并回填表单。Refine 不会自动为列表的每一行渲染<EditButton>,需要你手动放在操作列中:

import { EditButton, List, useModalForm, useTable } from "@refinedev/antd"; import { Form, Input, Modal, Select, Space, Table } from "antd"; const PostList: React.FC = () => { const { tableProps } = useTable<IPost>(); const { modalProps: editModalProps, formProps: editFormProps, show: editModalShow, } = useModalForm<IPost>({ action: "edit", warnWhenUnsavedChanges: true, // 有未保存修改时离开页面给出警告 }); return ( <> <List> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column dataIndex="status" title="Status" /> <Table.Column<IPost> title="Actions" dataIndex="actions" key="actions" render={(_, record) => ( <Space> <EditButton hideText size="small" recordItemId={record.id} onClick={() => editModalShow(record.id)} /> </Space> )} /> </Table> </List> <Modal {...editModalProps}> <Form {...editFormProps} layout="vertical"> {/* 与创建弹窗相同的 Form.Item 结构 */} </Form> </Modal> </> ); };

编辑模式下,表单内部会挂载useEditForm/useForm的数据查询逻辑,formProps中的initialValues会在记录数据到达后自动设置,因此你不需要手动处理useEffect回填,弹窗打开时会先进入加载状态(可通过formLoading配合<Spin>显示)。示例代码中正是这样处理的:

<Modal {...editModalProps}> <Spin spinning={editFormLoading}> <Form {...editFormProps} layout="vertical">...</Form> </Spin> </Modal>

注意:不要忘记把记录id传给show,这是editclone两种模式获取记录数据的必要条件。

克隆弹窗:复制一条已有记录

action: "clone"edit结构相同,差异在于:它会加载目标记录的数据填入表单,但提交时调用的是 data provider 的create方法,从而生成一条新的记录——实现"以某条数据为模板新建"的常见需求。同样需要手动放置<CloneButton>并把record.id传入show

import { CloneButton, List, useModalForm, useTable } from "@refinedev/antd"; import { Form, Input, Modal, Select, Space, Table } from "antd"; const PostList: React.FC = () => { const { tableProps } = useTable<IPost>(); const { modalProps: cloneModalProps, formProps: cloneFormProps, show: cloneModalShow, } = useModalForm<IPost>({ action: "clone", }); return ( <> <List> <Table {...tableProps} rowKey="id"> {/* ...省略列表列定义... */} <Table.Column<IPost> title="Actions" dataIndex="actions" key="actions" render={(_, record) => ( <Space> <CloneButton hideText size="small" recordItemId={record.id} onClick={() => cloneModalShow(record.id)} /> </Space> )} /> </Table> </List> <Modal {...cloneModalProps}> <Form {...cloneFormProps} layout="vertical">...</Form> </Modal> </> ); };

三个模式总结如下:

action 值数据来源提交行为show() 是否需要 id
createdata provider 的create
edit按 id 调用getOne回填data provider 的update
clone按 id 调用getOne回填data provider 的create

与 URL 同步:syncWithLocation

syncWithLocationuseModalForm最实用的增强能力之一。当设为true时,弹窗的可见状态以及当前记录的id会与 URL 查询参数保持同步,默认值为false

const modalForm = useModalForm({ syncWithLocation: true, });

该属性也可以配置为对象形式{ key: string; syncId?: boolean },用于自定义 URL 查询参数的 key,并且只有syncIdtrueid才会同步到 URL:

const modalForm = useModalForm({ syncWithLocation: { key: "my-modal", syncId: true }, });

从源码看,syncWithLocation的实现非常精细(useModalForm.ts#L147-L256):

  • 同步 key 的默认生成规则是`modal-${identifier}-${action}`,例如资源posts的编辑弹窗对应modal-posts-edit
  • 首次挂载时,会从useParsed()解析出的 URL 参数中读取open(布尔值或字符串"true"都会触发show())与id(触发setId);
  • 之后每次可见状态或id变化,都会通过useGo()type: "replace"的方式写回 URL(可见时写入{ open: true, id },关闭时移除该参数),保证浏览器前进/后退可以还原弹窗状态。

在示例应用 App.tsx 中,全局也开启了options.syncWithLocation: true,两者配合后,刷新页面时弹窗与当前编辑的记录可以完整恢复,非常适合可分享 URL 的管理后台场景。

核心配置项详解

useModalForm继承useForm的全部 props(详见 useForm 文档 的 Properties 章节),这里重点讲解与弹窗行为和增强能力相关的配置。

defaultFormValues

用于预填充表单的默认值:

useModalForm({ defaultFormValues: { title: "Hello World", }, });

也可以传入一个异步函数来获取默认值,加载期间可通过返回的defaultFormValuesLoading跟踪状态:

const { defaultFormValuesLoading } = useModalForm({ defaultFormValues: async () => { const response = await fetch("https://my-api.com/posts/1"); const data = await response.json(); return data; }, });

需要留意的是:当actioneditclone时,异步defaultFormValues与记录查询之间存在竞态条件,表单值将是最后完成的那次操作的结果,因此在这两种模式下要谨慎使用异步默认值。

defaultVisible

是否默认打开弹窗,默认false

const modalForm = useModalForm({ defaultVisible: true, });

源码中该值会透传给内部useModal的初始open状态(useModalForm.ts#L184-L188)。

autoSubmitClose

提交成功后是否自动关闭弹窗,默认true

const modalForm = useModalForm({ autoSubmitClose: false, });

autoResetForm

提交成功后是否重置表单字段,默认true

const modalForm = useModalForm({ autoResetForm: false, });

autoResetFormWhenClose

弹窗关闭时是否重置表单,默认true

const modalForm = useModalForm({ autoResetFormWhenClose: false, });

在源码的handleClose中,关闭弹窗时会setId(undefined)并调用form.resetFields()(仅在autoResetFormWhenClose为 true 时),确保下次打开时不会残留上一次的数据(useModalForm.ts#L266-L297)。

warnWhenUnsavedChanges

设为true后,用户在带未保存修改的情况下尝试离开页面时会弹出确认警告,默认false。该值也可以在<Refine>组件的options.warnWhenUnsavedChanges中全局设置,useModalForm的局部值会覆盖全局默认值(示例 App.tsx 即开启了全局配置,并配合UnsavedChangesNotifier组件使用):

const modalForm = useModalForm({ warnWhenUnsavedChanges: true, });

overtimeOptions

当请求耗时过长时,用于展示"加载超时"提示。interval为轮询间隔(毫秒),onInterval为每次间隔触发的回调;Hook 返回的overtime.elapsedTime表示已耗时(毫秒),请求完成后变为undefined

const { overtime } = useModalForm({ overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); // 在渲染中这样使用: { overtime.elapsedTime >= 4000 && <div>this takes a bit longer than expected</div> }

autoSave

自动保存功能,仅在edit模式下生效:当用户停止编辑超过防抖时间后自动触发保存,创建模式下仍需手动提交。

useModalForm({ autoSave: { enabled: true, }, });

autoSave 对象支持以下子配置:

  • debounce:防抖时间,默认1000毫秒,例如debounce: 2000表示停止输入 2 秒后保存;
  • onFinish:提交前修改数据的回调,例如onFinish: (values) => ({ foo: "bar", ...values })
  • invalidateOnUnmount:Hook 卸载时是否失效当前资源的listmanydetail查询,默认false;可通过invalidates选择要失效的查询类型;
  • invalidateOnClose:弹窗关闭时是否失效上述查询,默认false
  • onMutationSuccess / onMutationError:保存成功/失败回调,可通过isAutoSave参数判断触发来源是否为 autoSave。

启用后,Hook 会额外返回autoSaveProps(包含 mutation 的dataerrorstatus)。

返回值详解

useModalForm返回useForm的全部返回值,外加一组弹窗专属 API:

返回值类型说明
formPropsFormProps展开到<Form>,内含onValuesChangeinitialValuesonFinish
modalPropsModalProps展开到<Modal>,内含opentitleokTextonOkonCancel
show(id?: BaseKey) => void打开弹窗;edit/clone模式需传记录id
close() => void关闭弹窗
openboolean弹窗当前是否可见
submit() => void手动触发表单提交
formLoadingboolean表单数据加载状态
formFormInstance<TVariables>Ant Design 表单实例
id/setIdBaseKey \| undefined当前编辑的记录 id 及其 setter
queryQueryObserverResult记录查询结果
mutationUseMutationResult表单提交触发的 mutation
overtime{ elapsedTime?: number }超时信息
autoSaveProps对象autoSave 的 mutation 状态
defaultFormValuesLoadingboolean异步默认值加载状态

formProps

formProps来自底层的useForm,负责管理<Form>的状态与行为。一个重要的区别是:

useModalForm直接返回的onFinishuseFormonFinish相同;而formProps.onFinish在其基础上扩展了提交后的操作——内部会依次执行onFinish(values)、按autoSubmitClose关闭弹窗、按autoResetForm重置字段。

源码中这一逻辑清晰可见(useModalForm.ts#L327-L338)。因此,如果你需要自定义提交数据,推荐通过formProps.onFinish包装,让 Hook 继续接管提交后的关闭与重置。

modalProps

modalProps<Modal>提供完整的行为配置(useModalForm.ts#L339-L354):

  • title:根据资源与 action 自动生成,例如Create PostEdit Post,支持 i18n(key 为`${identifier}.titles.${action}`);
  • okText:确定按钮文案,默认"Save"(i18n keybuttons.save);
  • cancelText:取消按钮文案,默认"Cancel"
  • width:弹窗宽度,默认1000px
  • forceRender:是否强制渲染而非懒加载,默认true
  • okButtonProps:确定按钮的完整 props(disabledloading等),其onClick会触发form.submit()
  • onOk:手动提交<Form>的函数;
  • onCancel:手动关闭弹窗的函数(等价于close,内部会处理未保存警告与表单重置)。

open / close / show / submit

这四个返回值用于完全手动控制弹窗生命周期。例如,用close在自定义提交逻辑后手动关窗:

const { close, modalProps, formProps, onFinish } = useModalForm(); const onFinishHandler = async (values) => { // Awaiting onFinish 很重要,未保存更改提示、缓存失效、重定向等都依赖它 await onFinish(values); close(); }; return ( <Modal {...modalProps}> <Form {...formProps} onFinish={onFinishHandler} layout="vertical"> <Form.Item label="Title" name="title"> <Input /> </Form.Item> </Form> </Modal> );

submit在自定义 footer 中触发提交:

const { modalProps, formProps, submit } = useModalForm(); return ( <Modal {...modalProps} footer={[ <Button key="submit" type="primary" onClick={submit}> Submit </Button>, ]} > <Form {...formProps} layout="vertical"> {/* Form.Item... */} </Form> </Modal> );

show()从任意按钮打开弹窗:

const { modalProps, formProps, show } = useModalForm(); return ( <> <Button type="primary" onClick={() => show()}> Show Modal </Button> <Modal {...modalProps}>...</Modal> </> );

底层实现原理

从 useModalForm.ts 的源码可以看到整个 Hook 的组合方式:

  1. 基于useForm组装:调用useForm拿到formformPropsidsetIdformLoadingonFinishautoSaveProps,并把autoSaveinvalidates及其余 props 原样透传;
  2. 内部useModal状态管理:通过@hooks/modaluseModal管理open状态,modalProps.open初始值来自defaultVisible
  3. handleShow守卫edit/clone模式下,只有拿到showId或已有id时才真正打开弹窗,避免"无 id 打开编辑框";
  4. handleClose收尾:autoSave 且invalidateOnClose时失效查询 → 未保存警告确认 → 清空id→ 关闭弹窗 → 按需重置表单;
  5. URL 双向同步:初始化时从 URL 恢复open/id,状态变化时写回 URL;
  6. 合并返回:把useForm返回值与弹窗状态合并,并覆写formProps.onFinishmodalProps的默认文案与行为。

仓库中还提供了针对该 Hook 的单元测试 packages/antd/src/hooks/form/useModalForm/index.spec.tsx,覆盖了三种 action 下的渲染、提交与弹窗行为,可作为深入理解其内部契约的参考。

FAQ:提交前如何修改数据?

需求场景:用户填写了namesurname两个输入框,但 API 期望提交fullName字段。此时用formProps.onFinish包装一层转换即可,让 Hook 继续负责提交后的关闭与重置:

import { Modal, useModalForm } from "@refinedev/antd"; import { Form, Input } from "antd"; import React from "react"; export const UserCreate: React.FC = () => { const { formProps, modalProps } = useModalForm({ action: "create", }); const handleOnFinish = (values) => { formProps.onFinish?.({ fullName: `${values.name} ${values.surname}`, }); }; return ( <Modal {...modalProps}> <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> </Modal> ); };

类型参数说明

useModalForm支持泛型参数,用于约束数据与错误类型:

类型参数说明默认值
TQueryFnData查询函数返回的数据类型,需继承BaseRecordBaseRecord
TError自定义错误类型,需继承HttpErrorHttpError
TVariables提交参数类型{}
TDataselect函数返回的数据类型TQueryFnData
TResponsemutation 返回的数据类型TData
TResponseErrormutation 的错误类型TError

两个值得注意的默认行为:

  • 标注*的 props 在RefineContext中有默认值,也可在<Refine>组件上设置,useModalForm的局部值会覆盖全局默认;
  • 标注**redirect:若未显式配置,action: "create"时默认跳转到该资源的edit页,action: "edit"时默认跳转到list

运行示例项目

示例应用 form-antd-use-modal-form 展示了"创建 + 编辑 + 查看"三种弹窗的完整组合(list.tsx 中同时实例化了createedit两个useModalForm,并通过useShow实现只读查看弹窗)。其技术栈与配置如下:

  • 依赖(package.json):@refinedev/corev5、@refinedev/antdv6、antdv5、reactv19、react-routerv7、@refinedev/simple-restv6,Node 版本要求>=20
  • 数据源:示例通过dataProvider(API_URL)连接https://api.fake-rest.refine.dev(App.tsx),simple-rest数据提供器位于 packages/simple-rest;
  • 本地运行:
cd examples/form-antd-use-modal-form npm install npm run dev

也可以在项目根目录使用 Refine CLI 直接拉取该示例:

npm create refine-app@latest -- --example form-antd-use-modal-form

启动后访问/posts页面,即可看到列表上方的"新建"按钮、行内编辑按钮与查看按钮,点击后分别弹出对应的 Ant Design Modal 表单。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询