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
React 18 Concurrent Mode
最后

getContainer 的一些变化

2022-12-08
@zombieJ

文章被以下专栏收录:

antd

Ant Design

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

Ant Design

Ant Design 官方专栏
我有想法,去参与讨论
文档贡献者
  • Tree 的勾选传导组件级别的 CSS-in-JS

    相关资源

    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 开源社区

    在网页开发中,我们时常会遇到弹出元素的需求,比如 Select 的下拉框、或者是 Modal 组件。直接将其渲染到当前节点下时,可能会被父节点的 overflow: hidden 裁剪掉:

    Overflow

    因而在 Ant Design 中,我们默认将其渲染到 body 下,但是这又会带来新的问题。由于不在同一个容器下,当用户滚动屏幕时会发现弹出层并未跟随滚动:

    Scroll

    为了解决这个问题,我们提供了 getContainer 属性,让用户可以自定义渲染的容器。getContainer 方法会在组件挂载时调用,返回一个容器节点,组件会通过 createPortal 渲染到这个节点下。

    tsx
    // Fake Code. Just for Demo
    const PopupWrapper = () => {
    const eleRef = React.useRef<HTMLDivElement>(null);
    React.useEffect(() => {
    // It's much complex with timing in real world. You can view the source for more detail:
    // https://github.com/react-component/portal/blob/master/src/Portal.tsx
    const container: HTMLElement = getContainer(eleRef.current);
    // ...
    }, []);
    return (
    <div ref={eleRef}>
    {...}
    </div>
    );
    }
    tsx
    // Fake Code. Just for Demo
    const defaultGetContainer = () => {
    const div = document.createElement('div');
    document.body.appendChild(div);
    return div;
    };
    const SomeComponent = ({ getContainer = defaultGetContainer }) => (
    <PopupWrapper getContainer={getContainer} />
    );

    我们暂时不关注 getContainer 需要动态切换挂载节点的需求(其实在过去很长时间它的确也无法切换),仅仅从 React 18 看,它遇到了一些问题。

    React 18 Concurrent Mode

    React 18 中,effect 可能会多次触发。为了防止不经意间破坏开发者的行为,在 StrictMode 下它也做了相应的调整:

    • React mounts the component.
      • Layout effects are created.
      • Effect effects are created.
    • React simulates effects being destroyed on a mounted component.
      • Layout effects are destroyed.
      • Effects are destroyed.
    • React simulates effects being re-created on a mounted component.
      • Layout effects are created
      • Effect setup code runs

    简单理解就是 StrictMode 下,即便你的 deps 里是空对象,effect 仍然会多次触发。在切换为 React 18 StrictMode 的时候,我们会发现在 HTML 中会成对出现挂载节点,同时前一个是空的:

    html
    <body>
    <div id="root">...</div>
    <!-- Empty -->
    <div className="sample-holder"></div>
    <!-- Real in use -->
    <div className="sample-holder">
    <div className="ant-component-wrapper">...</div>
    </div>
    </body>

    因而我们调整了调用实现,默认的 getContainer 也通过 state 进行管理,确保在 StrictMode 下会清理前一个 effect 生成的节点:

    tsx
    // Fake Code. Just for Demo
    const SomeComponent = ({ getContainer }) => {
    const [myContainer, setMyContainer] = React.useState<HTMLElement | null>(null);
    React.useEffect(() => {
    if (getContainer) {
    setMyContainer(getContainer());
    return;
    }
    const div = document.createElement('div');
    document.body.appendChild(div);
    setMyContainer(div);
    return () => {
    document.body.removeChild(div);
    };
    }, [getContainer]);
    return <PopupWrapper getContainer={() => myContainer} />;
    };

    将 getContainer 放入 effect 管理后,我们可以更符合 React 生命周期的方式去管理节点,同时也可以在 getContainer 变化时进行清理。从而支持动态改变 getContainer 的场景(虽然我个人比较怀疑这种使用场景的普遍性)。

    最后

    由于修复了 getContainer 不支持动态改变的问题,它也引入了一个潜在的 breaking change。开发者如果自定义 getContainer 每次都是创建新的 DOM 节点时,它就会因为 effect 不断执行,导致节点不断创建而死循环。如果你使用了这种方式并且遇到了问题,需要注意检查。