logoAnt Design

⌘ K
  • 디자인
  • 개발
  • 컴포넌트
  • 블로그
  • 자료
5.21.3
  • 为什么禁用日期这么难?
  • Why is it so hard to disable the date?
  • 封装 Form.Item 实现数组转对象
  • HOC Aggregate FieldItem
  • 行省略计算
  • Line Ellipsis Calculation
  • 📢 v4 维护周期截止
  • 📢 v4 surpassed maintenance period
  • Type Util
  • 一个构建的幽灵
  • A build ghost
  • 当 Ant Design 遇上 CSS 变量
  • Ant Design meets CSS Variables
  • API 기술 부채
  • 생동감 있는 Notification
  • 色彩模型与颜色选择器
  • Color Models and Color Picker
  • 主题拓展
  • Extends Theme
  • 虚拟表格来了!
  • Virtual Table is here!
  • Happy Work 테마
  • Happy Work Theme
  • 동적 스타일은 어디로 갔을까?
  • Suspense 引发的样式丢失问题
  • Suspense breaks styles
  • Bundle Size Optimization
  • 안녕, GitHub Actions
  • 所见即所得
  • To be what you see
  • 정적 메서드의 고통
  • SSR에서 정적 스타일 추출
  • SSR Static style export
  • 의존성 문제 해결
  • 贡献者开发维护指南
  • Contributor development maintenance guide
  • 转载-如何提交无法解答的问题
  • Repost: How to submit a riddle
  • 新的 Tooltip 对齐方式
  • Tooltip align update
  • Unnecessary Rerender
  • 如何成长为 Collaborator
  • How to Grow as a Collaborator
  • Funny Modal hook BUG
  • Modal hook 的有趣 BUG
  • about antd test library migration
  • antd 测试库迁移的那些事儿
  • Tree 的勾选传导
  • Tree's check conduction
  • getContainer 的一些变化
  • Some change on getContainer
  • Component-level CSS-in-JS

Modal hook 的有趣 BUG

Resources

Ant Design Charts
Ant Design Pro
Ant Design Pro Components
Ant Design Mobile
Ant Design Mini
Ant Design Landing-Landing Templates
Scaffolds-Scaffold Market
Umi-React Application Framework
dumi-Component doc generator
qiankun-Micro-Frontends Framework
ahooks-React Hooks Library
Ant Motion-Motion Solution
China Mirror 🇨🇳

Community

Awesome Ant Design
Medium
Twitter
yuque logoAnt Design in YuQue
Ant Design in Zhihu
Experience Cloud Blog
seeconf logoSEE Conf-Experience Tech Conference
Work with Us

Help

GitHub
Change Log
FAQ
Bug Report
Issues
Discussions
StackOverflow
SegmentFault

Ant XTech logoMore Products

yuque logoYuQue-Document Collaboration Platform
AntV logoAntV-Data Visualization
Egg logoEgg-Enterprise Node.js Framework
Kitchen logoKitchen-Sketch Toolkit
Galacean logoGalacean-Interactive Graphics Solution
xtech logoAnt Financial Experience Tech
Theme Editor
Made with ❤ by
Ant Group and Ant Design Community
loading

最近我们遇到了一个 issue,说是 Modal.useModal 的 contextHolder 在放置不同的位置时,modal.confirm 弹出位置会不一样:

import React from 'react';
import { Button, Modal } from 'antd';
export default () => {
const [modal, contextHolder] = Modal.useModal();
return (
<div>
<Modal open>
<Button
onClick={() => {
modal.confirm({ title: 'Hello World' });
}}
>
Confirm
</Button>
{/* 🚨 BUG when put here */}
{contextHolder}
</Modal>
{/* ✅ Work as expect when put here */}
{/* {contextHolder} */}
</div>
);
};

正常版本:

Normal

有问题版本:

BUG

从上图可以看到当 contextHolder 放在 Modal 内部时,hooks 调用的弹出位置不正确了。

思路整理

antd 的 Modal 底层调用的是 rc-dialog 组件库,其接受一个 mousePosition 属性,用于控制弹出位置(Dialog/Content/index.tsx):

// pseudocode
const elementOffset = offset(dialogElement);
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
mousePosition.y - elementOffset.top
}px`;

其中 offset 方法用于获取窗体本身的坐标位置(util.ts):

// pseudocode
function offset(el: Element) {
const { left, top } = el.getBoundingClientRect();
return { left, top };
}

通过断点调试,我们可以发现 mousePosition 的值是正确的,但是 offset 中获取的 rect 的值是错误的:

{
"left": 0,
"top": 0,
"width": 0,
"height": 0
}

这个值很明显代表窗体组件在动画启动节点尚未添加到 DOM 树中,所以我们需要查看一下 Dialog 添加的逻辑。

createPortal

rc-dialog 通过 rc-portal 在 document 中创建一个节点,然后通过 ReactDOM.createPortal 将组件渲染到这个节点上。对于 contextHolder 位置不同而出现表现不同可以推测,一定是在 document 创建节点的时序出现了问题,于是我们可以进一步看一下 rc-portal 中默认添加节点的部分(useDom.tsx):

// pseudocode
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
}
useLayoutEffect(() => {
if (queueCreate) {
queueCreate(append);
} else {
append();
}
}, []);

其中 queueCreate 是通过 context 获取,目的是为了防止在嵌套层级下,子元素创建先于父元素的情况:

<Modal title="Hello 1" open>
<Modal title="Hello 2" open>
<Modal>
<Modal>
<!-- Child `useLayoutEffect` is run before parent. Which makes inject DOM before parent -->
<div data-title="Hello 2"></div>
<div data-title="Hello 1"></div>

通过 queueCreate 将子元素的 append 加入队列,然后再通过 useLayoutEffect 执行:

// pseudocode
const [queue, setQueue] = useState<VoidFunction[]>([]);
function queueCreate(appendFn: VoidFunction) {
setQueue((origin) => {
const newQueue = [appendFn, ...origin];
return newQueue;
});
}
useLayoutEffect(() => {
if (queue.length) {
queue.forEach((appendFn) => appendFn());
setQueue([]);
}
}, [queue]);

问题分析

由于上述的队列操作,使得 portal 的 DOM 在嵌套下会在下一个 useLayoutEffect 触发。这导致添加节点行为后于 rc-dialog 启动动画的 useLayoutEffect 时机,导致元素不在 document 中而无法获取正确的坐标信息。

由于 Modal 已经是开启状态,其实不需要通过 queue 异步执行,所以我们只需要加一个判断如果是开启状态,直接执行 append 即可:

// pseudocode
const appendedRef = useRef(false);
const queueCreate = !appendedRef.current
? (appendFn: VoidFunction) => {
// same code
}
: undefined;
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
appendedRef.current = true;
}
// ...
return <PortalContext value={queueCreate}>{children}</PortalContext>;

以上。