logoAnt Design

⌘ K
  • 设计
  • 研发
  • 组件
  • 博客
  • 资源
  • 国内镜像
5.25.4
  • v6 的一些 CSS 琐事
  • 👀 视觉回归测试
  • 为什么禁用日期这么难?
  • 封装 Form.Item 实现数组转对象
  • 行省略计算
  • 📢 v4 维护周期截止
  • antd 里常用的 TypeScript 工具方法
  • 一个构建的幽灵
  • 当 Ant Design 遇上 CSS 变量
  • API 的历史债务
  • 灵动的 Notification
  • 色彩模型与颜色选择器
  • 主题拓展
  • 虚拟表格来了!
  • 快乐工作主题(一)
  • 动态样式去哪儿了?
  • Suspense 引发的样式丢失问题
  • 打包体积优化
  • 你好,GitHub Actions
  • 所见即所得
  • 静态方法之痛
  • SSR 静态样式导出
  • 依赖排查
  • 贡献者开发维护指南
  • 转载-如何提交无法解答的问题
  • 新的 Tooltip 对齐方式
  • 非必要的渲染
  • 如何成长为 Collaborator
  • Modal hook 的有趣 BUG
  • antd 测试库迁移的那些事儿
  • Tree 的勾选传导
  • getContainer 的一些变化
  • 组件级别的 CSS-in-JS

Modal hook 的有趣 BUG

2022-12-21
@zombieJ

文章被以下专栏收录:

antd

Ant Design

一个 UI 设计体系
我有想法,去参与讨论
antd

Ant Design

Ant Design 官方专栏
我有想法,去参与讨论
antd

Ant Design

Juejin logoAnt Design 开源专栏
Juejin logo我有想法,去参与讨论
文档贡献者
  • 如何成长为 Collaboratorantd 测试库迁移的那些事儿

    相关资源

    Ant Design X
    Ant Design Charts
    Ant Design Pro
    Pro Components
    Ant Design Mobile
    Ant Design Mini
    Ant Design Web3
    Ant Design Landing-首页模板集
    Scaffolds-脚手架市场
    Umi-React 应用开发框架
    dumi-组件/文档研发工具
    qiankun-微前端框架
    Ant Motion-设计动效
    国内镜像站点 🇨🇳

    社区

    Awesome Ant Design
    Medium
    Twitter
    yuque logoAnt Design 语雀专栏
    Ant Design 知乎专栏
    体验科技专栏
    seeconf logoSEE Conf-蚂蚁体验科技大会
    加入我们

    帮助

    GitHub
    更新日志
    常见问题
    报告 Bug
    议题
    讨论区
    StackOverflow
    SegmentFault

    Ant XTech logo更多产品

    yuque logo语雀-构建你的数字花园
    AntV logoAntV-数据可视化解决方案
    Egg logoEgg-企业级 Node.js 框架
    Kitchen logoKitchen-Sketch 工具集
    Galacean logoGalacean-互动图形解决方案
    xtech logo蚂蚁体验科技
    主题编辑器
    Made with ❤ by
    蚂蚁集团和 Ant Design 开源社区

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

    tsx
    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):

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

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

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

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

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

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

    createPortal

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

    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 获取,目的是为了防止在嵌套层级下,子元素创建先于父元素的情况:

    tsx
    <Modal title="Hello 1" open>
    <Modal title="Hello 2" open>
    <Modal>
    <Modal>
    html
    <!-- 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 执行:

    tsx
    // 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 即可:

    tsx
    // 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>;

    以上。