React Portals:将子节点渲染到其他DOM节点

1,549 阅读3分钟

使用 React 时,一般来说,使用函数组件return一个元素时,该元素是被挂载到其最近的 DOM 父节点下,例如:

return (
  <div>
    {this.props.children}
  </div>
);

挂载了一个新的div元素,其子元素被渲染在此div中。

但是,我们需要将子元素挂载到其他的任意 DOM 节点下。

例如,最近在实习中遇到一个的问题,要写一个鼠标悬浮时向下弹出选择框的效果。实际效果却是选择框向下弹出,但只显示出了一部分,有一部分不可见。

打开控制台后发现是由于上层组件元素使用了overflow: hidden,导致溢出部分被隐藏了。本来想写一个样式覆盖掉overflow: hidden,后来发现上层组件几乎每一层都使用了overflow: hidden。组件设计时就默认这么写的,不可能每一层都去改样式😂。

这时候,我们就需要将子元素挂载到其他的 DOM 节点下。绕过上层使用了overflow: hidden的元素,直接挂载到更上层。后来通过 Portals 就解决了这个问题。

作用

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

以上是React官网对 Portal 的描述。简单来说,可以将子元素渲染到其他的 DOM 节点。

用法

ReactDOM.createPortal(child, container)
  • child:ReactNode,可渲染的 React 子元素
  • container:Element,DOM 元素

示例

如下代码所示,App组件是一个大盒子,App组件下有一个子组件PortalTest

const App = () => {
  return (
    <div className={s.AppBox} id='app'>
      <PortalTest />
    </div>
  );
};

子组件PortalTest的定义如下:

const PortalTest = () => {
  return (
    <div className={s.A}>
      <div className={s.B}></div>
    </div>
  );
};
.A {
  position: relative;
  overflow: hidden;
  width: 100px;
  height: 100px;
  background-color: rgb(228, 228, 228);
}

.B {
  position: absolute;
  width: 200px;
  height: 40px;
  background-color: rgb(113, 215, 247);
  top: 50%;
  transform: translate(0, -50%);
}

A是一个小盒子,B是一个小长条,B的宽度比A长,但由于A设置了overflow: hiddenB超出的部分将隐藏,如图所示:

真实 DOM 结构如图所示:

解决方式为,使用ReactDOM.createPortal(),将B挂载到App下,跳过A

const PortalTest = () => {
  const [node, setNode] = useState<ReactPortal>();

  useEffect(() => {
    const test = <div className={s.B}></div>;
    const app = document.getElementById('app') as Element;
    setNode(ReactDOM.createPortal(test, app));
  }, []);

  return <div className={s.A}>{node}</div>;
};

如上代码所示,使用ReactDOM.createPortal(test, app)渲染B,挂载到idapp的 DOM 节点(App组件)下。这里使用useEffect的原因是,要在PortalTest插入到 DOM 树中,才能渲染子元素。

效果如下,由于B跳过了A,直接挂载到App组件的节点下,所以B完全展示出来了,不受Aoverflow: hidden的影响。

真实 DOM 结构如图所示:

冒泡

虽然通过 Portal 可以将子元素挂载到其他的 DOM 节点下,但在其他的任何方面,其行为和普通的 React 子节点行为一致。比如事件冒泡机制,某元素的子元素挂载到其他的 DOM 节点,这个子元素触发的事件,还是会冒泡到该元素上,并不会冒泡至挂载到 DOM 节点。

如下代码所示,有id分别为AB的两个容器,A容器下有A盒子,B盒子虽然在A容器里,但被挂载到B容器中:

const App = () => {
  const [node, setNode] = useState<ReactPortal>();

  useEffect(() => {
    const boxB = <div className={s.box}>B</div>;
    const B = document.getElementById('B') as Element;
    setNode(ReactDOM.createPortal(boxB, B));
  }, []);

  const propagationA = () => {
    console.log('A');
  };
  const propagationB = () => {
    console.log('B');
  };

  return (
    <>
      <div id='A' onClick={propagationA}>
        <div className={s.box}>A</div>
        {node}
      </div>
      <div id='B' onClick={propagationB}></div>
    </>
  );
};

真实的 DOM 结构如图所示,B盒子确实被挂载到了B容器中:

AB两容器分别有click事件,以显示冒泡效果。结果发现,点击B盒子时,控制台也同样打印'A',这说明B盒子虽然被挂载到B容器下,但是B盒子触发的事件,还是会按照原来的机制进行冒泡,会冒泡到A容器上。


以上是本人学习所得之拙见,若有不妥,欢迎指出交流!

参考: