antd Modal 对话框组件完全指南:从声明式用法到静态方法与 Hooks 确认框
Modal(对话框)是 antd 反馈类组件中承载“二次确认与关键操作”的核心载体。本指南基于 components/modal/index.zh-CN.md 的完整 API 文档,并结合 Modal.tsx、confirm.tsx、useModal/index.tsx 等源码实现,系统讲解三套 API 形态(受控组件、静态方法、useModal Hooks)的用法差异、全部关键属性、焦点与遮罩等新能力,以及 context 丢失等高频 FAQ 的底层成因与解法,帮助你写出真正可用、可维护的确认弹窗。
一、何时使用 Modal
当用户需要处理某个事务(提交、删除、确认重要变更),又不希望跳转页面打断当前工作流时,可以在当前页面正中打开一个浮层来承载操作——这正是 Modal 的核心使用场景。antd 提供两种入口:
- 受控组件形态:直接渲染
<Modal open={...} title="..." onOk={...} onCancel={...}>,适用于页面内嵌的复杂表单、详情展示等需要自定义内部结构的内容。 - 语法糖形态:通过
App.useApp或Modal.confirm等静态方法弹出一个“问一句就够”的简洁确认框,免去维护大量样板状态。
对应的可运行示例分别位于 demo/basic.tsx(基础用法)与 demo/confirm.tsx(静态确认框)。
二、代码演示概览
组件文档通过一组 demo 覆盖了从入门到高级的完整能力矩阵,源码都位于 components/modal/demo 目录下,可作为直接的“抄作业”素材:
| 能力主题 | 演示文件 | 引入版本 |
|---|---|---|
| 基本受控用法、异步关闭 | basic.tsx、async.tsx | - |
| 自定义页脚 / 页脚渲染函数 | footer.tsx、footer-render.tsx | footer 函数式 5.9.0 |
| 遮罩细分控制(enabled/blur/closable) | mask.tsx | mask.closable 6.3.0 |
| 加载骨架屏 | loading.tsx | 5.18.0 |
| Hooks 上下文弹窗 | hooks.tsx | - |
| 国际化 | locale.tsx | - |
| 手动 update / destroy、自定义位置与宽度 | manual.tsx、position.tsx、width.tsx | width 支持 Breakpoint 5.23.0 |
| 按钮属性、自定义渲染 | button-props.tsx、modal-render.tsx | - |
| 静态方法 / 静态确认框 / 路由销毁 | static-info.tsx、confirm.tsx、confirm-router.tsx | - |
| 语义化结构与 style | style-class.tsx | 6.0.0 |
| 动画原点、调试用线框等 | custom-mouse-position.tsx 等 | - |
其中
dark、nested、_InternalPanelDoNotUseOrYouWillBeFired(见 render-panel.tsx)、wireframe、component-token属于debug性质,仅供内部开发调试理解实现,不建议作为业务模板。
三、Modal 组件 API 详解(声明式受控用法)
声明式 API 的完整类型定义见 components/modal/interface.ts(ModalProps),其本质是在 @rc-component/dialog 的 DialogProps 之上扩展业务字段后由 Modal.tsx 消费。
通用属性参考:通用属性。
| 参数 | 说明 | 类型 | 默认值 | 版本/全局配置 |
|---|---|---|---|---|
| afterClose | Modal 完全关闭后的回调(关闭动画结束后触发) | function | - | × |
| cancelButtonProps | 取消按钮 props,透传给 Button,可覆写 disabled、size 等 |
ButtonProps | - | 全局配置 6.0.0 |
| cancelText | 取消按钮文字 | ReactNode | 取消 |
× |
| centered | 垂直居中展示(源码中会追加 ${prefixCls}-centered class,见 Modal.tsx) |
boolean | false | 全局 5.24.0 |
| classNames | 语义化结构自定义 class,支持对象或函数签名(详见下文 Semantic DOM) | Record<SemanticDOM, string> | (info) => Record<SemanticDOM, string> | - | 5.10.0 |
| closable | 是否显示右上角关闭按钮,传对象时可同时配置 closeIcon、onClose 等 | boolean | ClosableType | true | 5.16.0 |
| closeIcon | 自定义关闭图标;设为 null/false 隐藏关闭按钮 |
ReactNode | <CloseOutlined /> |
5.14.0 |
| confirmLoading | 确定按钮 loading(点击 ok 后进入 loading,可配合异步流程防重复提交) | boolean | false | × |
已废弃,请改用 destroyOnHidden |
boolean | false | × | |
| destroyOnHidden | 关闭时销毁 Modal 内子元素,保证每次打开重新挂载 | boolean | false | 5.25.0 |
已废弃,请用 focusable.focusTriggerAfterClose 替代 |
boolean | true | 4.9.0 | |
| footer | 底部内容;设为 footer={null} 去掉底部按钮区 |
ReactNode | (originNode, { OkBtn, CancelBtn }) => ReactNode | 确定/取消按钮 | 函数式 5.9.0 |
| forceRender | 强制渲染(打开前就渲染内部结构,便于预先获取内部 DOM/实例) | boolean | false | × |
| focusable | 对话框焦点管理:{ trap?: boolean, focusTriggerAfterClose?: boolean } |
object | - | 6.2.0(全局 6.4.0) |
| getContainer | Modal 挂载节点;默认全屏展示在 document.body;false 表示挂载在当前 DOM 位置 |
HTMLElement | () => HTMLElement | Selectors | false | document.body | × |
| keyboard | 是否支持 Esc 键关闭 | boolean | true | × |
| mask | 遮罩配置,支持 { enabled?: boolean, blur?: boolean, closable?: boolean },对象形式由 useMergedMask 解析合并(见 Modal.tsx) |
boolean | object | true | 6.0.0,closable 6.3.0 |
已废弃,请使用 mask.closable 替代 |
boolean | true | - | |
| modalRender | 自定义渲染对话框:(node) => ReactNode,可在外层包裹拖拽、缩放等能力(源码中会额外包一层 ${prefixCls}-render) |
function | - | 4.7.0 |
| okButtonProps | 确定按钮 props | ButtonProps | - | 全局 6.0.0 |
| okText | 确定按钮文字 | ReactNode | 确定 |
× |
| okType | 确定按钮类型 | string | primary |
× |
| style | 设置浮层样式(如调整位置、边距),注意它作用于根容器而非 body | CSSProperties | - | 全局 5.7.0 |
| styles | 语义化结构行内 style,支持对象或函数 | Record<SemanticDOM, CSSProperties> | (info) => ... | - | 5.10.0 |
| loading | 显示骨架屏(内部渲染 4 行 Skeleton,见 Modal.tsx) | boolean | false | 5.18.0 |
| scrollLock | 打开时是否锁定 body 滚动 | boolean | true | 6.5.0 |
| title | 标题 | ReactNode | - | × |
| open | 是否可见(受控属性,antd 5.x 已统一从 visible 迁移到 open) |
boolean | false | × |
| width | 宽度;传对象可按响应式断点设置不同宽度(Breakpoint,实现上转换为 CSS 变量,见 Modal.tsx) | string | number | Breakpoint | 520 | Breakpoint 5.23.0 |
| wrapClassName | 对话框外层容器(wrapper)类名 | string | - | × |
| zIndex | 设置 Modal 的 z-index(默认经 useZIndex 从上下文读取,见 Modal.tsx) |
number | 1000 | × |
| onCancel | 点击遮罩/右上角叉/取消按钮时的回调 | function(e) | - | × |
| onOk | 点击确定按钮时的回调 | function(e) | - | × |
| afterOpenChange | 打开与关闭动画结束后的回调 | (open: boolean) => void | - | 5.4.0 |
使用注意事项
- 默认不销毁内容:
<Modal />关闭后内部状态不会自动清空。若希望每次打开都是全新内容,请设置destroyOnHidden。关闭动画期间内容会被 memo,这也是 FAQ 中“关闭时内容不更新”现象的根源。 - 与 Form 配合:即使设置
destroyOnHidden,关闭时表单字段数据也不会自动销毁,还需要对<Form>设置preserve={false}。 - RTL 模式:
Modal.method()的 RTL(direction: rtl)支持仅限 hooks 用法(useModal),静态方法不受 ConfigProvider 的 direction 上下文影响。
遮罩(mask)新形态
mask 在较新版本已升级为对象配置:
<Modal
mask={{
enabled: true, // 是否显示遮罩
blur: true, // 是否对遮罩应用毛玻璃模糊效果(配合 blur 相关 class)
closable: true, // 点击遮罩是否关闭(替代旧的 maskClosable)
}}
...
/>
若同时传入旧的 maskClosable,开发环境下控制台会给出废弃提示(见 Modal.tsx 中的 warning.deprecated 列表),新版代码请统一使用 mask.closable。
ClosableType
当 closable 为对象时支持以下字段:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| afterClose | Modal 完全关闭后的回调 | function | - |
| closeIcon | 自定义关闭图标 | ReactNode | undefined |
| disabled | 关闭图标是否禁用(禁用时关闭按钮不可点击,源码中经 useClosable 得到 closeBtnIsDisabled) |
boolean | false |
| onClose | 弹窗即时关闭回调(点击关闭立即触发,无需等待动画) | Function | undefined |
<Modal
closable={{
disabled: false,
closeIcon: <CustomIcon />,
onClose: () => console.log('立即关闭'),
afterClose: () => console.log('动画结束后关闭'),
}}
>
...
</Modal>
四、静态方法:Modal.method()
除受控组件外,Modal 还以命名空间方式挂载了 5 个静态确认函数(挂载逻辑见 components/modal/index.tsx):
Modal.infoModal.successModal.errorModal.warning(同时保留旧别名Modal.warn)Modal.confirm
它们都接收一个 object 参数,底层统一由 confirm.tsx 的 confirm(config) 入口渲染确认对话框(withInfo/withSuccess 等只是把对应的 type 预置好),参数即 ModalFuncProps,类型见 interface.ts。
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| afterClose | Modal 完全关闭后的回调 | function | - | 4.9.0 |
已废弃,请使用 focusable.autoFocusButton 替代 |
null | ok | cancel |
ok |
- | |
| cancelButtonProps | 取消按钮 props | ButtonProps | - | - |
| cancelText | 取消按钮文字 | string | 取消 |
- |
| centered | 垂直居中展示 | boolean | false | - |
| className | 容器类名 | string | - | - |
| closable | 是否显示右上角关闭按钮 | boolean | ClosableType | false | - |
| closeIcon | 自定义关闭图标 | ReactNode | undefined | 4.9.0 |
| content | 内容 | ReactNode | - | - |
| focusable.autoFocusButton | 指定自动获得焦点的按钮 | null | ok | cancel |
ok |
6.2.0 |
| footer | 底部内容,footer: null 可去掉 |
ReactNode | 函数 | - | 函数式 5.9.0 |
| getContainer | 挂载 HTML 节点,false 为挂载在当前 DOM(静态方法不支持 false,开发环境会告警,见 confirm.tsx) |
HTMLElement | ()=>HTMLElement | Selectors | false | document.body | - |
| icon | 自定义图标 | ReactNode | <ExclamationCircleFilled /> |
- |
| keyboard | 是否支持 Esc 关闭 | boolean | true | - |
| mask | 遮罩配置 | boolean | object | true | - |
已废弃,请用 mask.closable 替代 |
boolean | false | - | |
| scrollLock | 打开时锁定 body 滚动 | boolean | true | 6.5.0 |
| okButtonProps | 确定按钮 props | ButtonProps | - | - |
| okText | 确定按钮文字 | string | 确定 |
- |
| okType | 确定按钮类型 | string | primary |
- |
| style | 浮层样式 | CSSProperties | - | - |
| title | 标题 | ReactNode | - | - |
| width | 宽度 | string | number | 416(确认框默认更窄) | - |
| wrapClassName | 外层容器类名 | string | - | 4.18.0 |
| zIndex | z-index | number | 1000 | - |
| onCancel | 点击取消回调,参数为关闭函数;返回 Promise 时 resolve 正常关闭、reject 不关闭 | function(close) | - | - |
| onOk | 点击确定回调,参数为关闭函数;返回 Promise 语义同上 | function(close) | - | - |
一个带 Promise 异步确认的经典写法:
Modal.confirm({
title: '确认删除这条数据吗?',
content: '删除后不可恢复,请谨慎操作。',
okText: '确认删除',
okType: 'danger',
onOk() {
return new Promise((resolve, reject) => {
// 模拟异步请求:成功 resolve 关闭,失败 reject 保持打开
setTimeout(resolve, 2000);
}).catch(() => console.log('Oops errors!'));
},
onCancel() {},
});
返回实例的 update / destroy
函数调用后会返回一个引用,通过该引用可以更新和关闭弹窗:
const modal = Modal.info();
modal.update({
title: '修改的标题',
content: '修改的内容',
});
// 4.8.0 及以上:支持传入函数,基于上一个配置做增量更新
modal.update((prevConfig) => ({
...prevConfig,
title: `${prevConfig.title}(新)`,
}));
modal.destroy();
update 的实现位于 confirm.tsx:函数参数会基于 currentConfig 计算出新配置,对象参数则浅合并;随后触发一次 scheduleRender,通过内部持有的 container(document.createDocumentFragment)重新渲染。而 destroy 本质是把 open 置为 false 并等待动画结束后的 afterClose 中卸载。
Modal.destroyAll():路由场景的批量销毁
Modal.destroyAll() 可以一次性销毁所有弹出的确认窗(info/success/error/warning/confirm)。它通常配合路由监听使用,解决路由前进、后退时确认框“残留”的问题——对“被动关闭”场景(路由变化)远比到处手动调用实例的 destroy() 更省心(modal.destroy() 更适用于主动关闭)。
import { browserHistory } from 'react-router';
// router change
browserHistory.listen(() => {
Modal.destroyAll();
});
其底层机制值得了解:每次 confirm() 创建弹窗时,会把对应的 close 函数 push 进全局数组 destroyFns(见 confirm.tsx);destroyAll 则循环弹出并依次调用,见 index.tsx 与 destroyFns.ts。
五、Modal.useModal():带 Context 的 Hooks 弹窗
当弹窗内容需要访问 Context(如主题、国际化、Redux 状态)时,直接调用静态方法是拿不到的(原因见 FAQ)。此时应使用 Modal.useModal:
const [modal, contextHolder] = Modal.useModal();
React.useEffect(() => {
modal.confirm({
// ...
});
}, []);
return <div>{contextHolder}</div>;
Hooks 形态的实现位于 useModal/index.tsx:内部维护一个 ElementsHolder 组件并通过 usePatchElement 把确认框作为普通 React 元素 patch 进 contextHolder,因此弹窗天然处于你书写 contextHolder 位置的 React 树中,能获取该位置的全部上下文(源码在渲染层面则复用 HookModal.tsx 封装 ConfirmDialog)。其 destroy/update 与静态方法实例一致,且针对 Modal 尚未挂载完成的场景用 actionQueue 做了动作排队。
modal.confirm 返回对象还额外支持 Promise 链式调用(then),这是 Hooks 版本独有的能力:
destroy:销毁当前弹窗update:更新当前弹窗then:Promise 链式调用,支持await。点击onOk时 resolvetrue,点击onCancel时 resolvefalse;此方法会把弹窗切换为“静默”模式(silent,避免 await 场景下再走二次确认逻辑,见 useModal/index.tsx)
const confirmed = await modal.confirm({ ... });
if (confirmed) {
// 用户点击了确定
} else {
// 用户点击了取消/关闭
}
对于业务中普遍存在“确认后还要用上下文发请求/弹 Toast”的场景,官方还推荐直接用 App 组件(App.useApp())包裹,可省去手动植入 contextHolder 的样板,message、notification、modal 三者统一由 Context 注入。
六、Semantic DOM:按语义结构精准定制样式
从 5.10.0 起,Modal 通过 classNames/styles 支持按“语义化节点”精准定制(区别于用 :global 硬改或给整弹窗套类名)。语义结构与演示代码见 demo/_semantic.tsx,完整的语义 key 定义在 interface.ts:
root:根节点header/title:头部区域与标题body:内容主体区footer:底部按钮区container/wrapper:容器与全屏包裹层mask:遮罩层close:右上角关闭按钮
classNames/styles 既支持普通对象,也支持函数签名 (info: { props }) => Record<...>,便于根据组件状态(如 open、confirmLoading)动态返回样式。合并逻辑由 useMergeSemantic 完成,且会与 ConfigProvider 中全局配置的 classNames/styles 进行合并(见 Modal.tsx)。6.0.0 起还提供了 style-class.tsx 演示更完整的语义样式用法。
同时需注意,旧的 bodyStyle、maskStyle 等平面化属性已废弃,统一迁移到 styles.body、styles.mask。
七、主题变量(Design Token)
Modal 的样式 Token(如 contentBg、headerBg、titleColor、footerBg、contentLineHeight 等,由 components/modal/style 下的样式实现消费)可通过 <ComponentTokenTable component="Modal"> 在组件文档页实时查看,并借助 ConfigProvider 的 theme.components.Modal 做全局定制。相关示例可参考 demo/component-token.tsx 与 demo/wireframe.tsx。
八、FAQ:高频问题与底层原理
1. 为什么 Modal 关闭时,内容不会更新?
Modal 在关闭动画过程中会把内容 memo 起来,避免关闭瞬间内容“跳动”导致视觉突兀。因此,如果你依赖“关闭时重置 Form 的 initialValues”这种方式刷新内容,是无效的;正确做法是在关闭后的 effect 中调用 form.resetFields() 显式重置,或使用 destroyOnHidden 让内容在关闭后真正卸载重建。
2. 为什么静态 Modal 方法拿不到 context、redux 和 ConfigProvider 的 locale/prefixCls/theme?
直接调用 Modal.confirm 等静态方法时,antd 会在组件树之外通过独立容器动态渲染出全新的 React 实例(见 confirm.tsx:创建 document.createDocumentFragment 后直接 render 到该容器),它与你当前代码所在的 React 树是两套实体,因此自然取不到调用处的 Context。静态方法能拿到的全局信息仅限 Modal.config 设置的 rootPrefixCls,该函数本身也已标记废弃,推荐改用 ConfigProvider.config(见 confirm.tsx)。
需要 Context 时请改用 Modal.useModal 返回的 modal + contextHolder,并把 contextHolder 放到你希望它继承上下文的 JSX 位置:
const [modal, contextHolder] = Modal.useModal();
return (
<Context1.Provider value="Ant">
{/* contextHolder 位于 Context1 内:弹窗能拿到 Context1 的 context */}
{contextHolder}
<Context2.Provider value="Design">
{/* contextHolder 位于 Context2 外:弹窗拿不到 Context2 的 context */}
</Context2.Provider>
</Context1.Provider>
);
异同小结:Hooks 创建的 contextHolder 必须真实插入到子元素节点中才生效;而当你确实不需要上下文时,直接调用静态方法更省事。两者共存的完整链路也可对照 demo hooks.tsx。
3. 静态方法如何设置 prefixCls?
通过 ConfigProvider.config 全局设置(其内部会写入 rootPrefixCls,静态弹窗渲染时据此拼出 ${rootPrefixCls}-modal 前缀,见 confirm.tsx)。
4. 动画原点来自哪里?
Modal 打开时的 zoom 动画默认从鼠标点击位置展开:源码在 document 上注册了捕获阶段的 click 监听,记录 100ms 内的最后一次点击坐标作为 mousePosition(见 Modal.tsx);如不想要该效果,可通过 mousePosition 传入自定义原点或 null 关闭。
结语:三套 API 怎么选
一句话总结实践取舍:
- 页面内嵌、内容复杂(表单、大段信息) → 使用受控
<Modal>,配destroyOnHidden、confirmLoading、footer定制与classNames/styles语义化样式; - 简单确认、不需要 Context →
Modal.confirm/info/success/error/warning静态方法,用返回值update/destroy精细控制,路由跳转场景用Modal.destroyAll(); - 需要 Context(主题/国际化/redux)、或想用
await modal.confirm()→Modal.useModal()或 App.useApp(),把contextHolder插入组件树对应位置。
掌握这三种形态与背后 Modal.tsx、confirm.tsx、useModal/index.tsx 的实现脉络,就足以应对从“页面弹窗”到“全局确认”的绝大多数交互场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00