createPortal

createPortal 让你可以将一些子元素渲染到 DOM 的不同部分。

<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>

参考

createPortal(children, domNode, key?)

要创建一个 portal,请调用 createPortal,传入一些 JSX,以及它应该渲染到的 DOM 节点:

import { createPortal } from 'react-dom';

// ...

<div>
<p>这个子元素被放置在父级 div 中。</p>
{createPortal(
<p>这个子元素被放置在 document body 中。</p>,
document.body
)}
</div>

查看更多示例。

portal 只会改变 DOM 节点的物理位置。除此之外,你渲染到 portal 中的 JSX 的行为,和作为渲染它的 React 组件的子节点并无不同。例如,子元素可以访问由父级树提供的上下文,并且事件会按照 React 树从子节点冒泡到父节点。

参数

  • children:任何可以由 React 渲染的内容,例如一段 JSX(比如 <div /><SomeComponent />)、一个 Fragment<>...</>)、字符串或数字,或者它们组成的数组。

  • domNode:某个 DOM 节点,例如 document.getElementById() 返回的节点。该节点必须已经存在。在更新期间传入不同的 DOM 节点会导致 portal 内容被重新创建。

  • 可选 key:用于作为 portal 的 key 的唯一字符串或数字。

返回值

createPortal 返回一个 React 节点,它可以包含在 JSX 中,或从 React 组件中返回。如果 React 在渲染输出中遇到它,它会将提供的 children 放入提供的 domNode 内。

注意事项

  • 来自 portal 的事件会按照 React 树而不是 DOM 树传播。例如,如果你在 portal 内点击,并且 portal 被 <div onClick> 包裹,那么该 onClick 处理函数会被触发。如果这会导致问题,可以选择阻止 portal 内部事件继续传播,或者将 portal 本身在 React 树中向上移动。

用法

渲染到 DOM 的不同部分

Portal 允许你的组件将其中一些子元素渲染到 DOM 中的不同位置。这让组件的一部分可以从它可能处于的任何容器中“逃逸”出来。例如,组件可以显示一个弹窗对话框或一个工具提示,使其显示在页面其余内容的上方和外部。

要创建一个 portal,请渲染 createPortal 的结果,并传入 一些 JSX 以及 它应该放置到的 DOM 节点

import { createPortal } from 'react-dom'; function MyComponent() { return ( <div style={{ border: '2px solid black' }}> <p>这个子元素被放置在父级 div 中。</p> {createPortal( <p>这个子元素被放置在 document body 中。</p>, document.body )} </div> ); }

React 会将 你传入的 JSX 对应的 DOM 节点放入 你提供的 DOM 节点 内。

如果没有 portal,第二个 <p> 会被放置在父级 <div> 中,但 portal 将它“传送”到了 document.body:

import { createPortal } from 'react-dom';

export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>这个子元素被放置在父级 div 中。</p>
      {createPortal(
        <p>这个子元素被放置在 document body 中。</p>,
        document.body
      )}
    </div>
  );
}

注意第二个段落在视觉上是如何显示在带边框的父级 <div> 外部的。如果你使用开发者工具检查 DOM 结构,你会看到第二个 <p> 被直接放置到了 <body> 中:

<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>这个子元素被放置在父级 div 中。</p>
</div>
...
</div>
<p>这个子元素被放置在 document body 中。</p>
</body>

portal 只会改变 DOM 节点的物理位置。除此之外,你渲染到 portal 中的 JSX 的行为,和作为渲染它的 React 组件的子节点并无不同。例如,子元素可以访问由父级树提供的上下文,并且事件仍会按照 React 树从子节点冒泡到父节点。


使用 portal 渲染模态对话框

你可以使用 portal 来创建一个悬浮在页面其余内容之上的模态对话框,即使调用该对话框的组件位于带有 overflow: hidden 或其他会干扰对话框样式的容器中。

在这个例子中,两个容器都有会破坏模态对话框的样式,但渲染到 portal 中的那个不会受影响,因为在 DOM 中,模态并不包含在父级 JSX 元素内。

import NoPortalExample from './NoPortalExample';
import PortalExample from './PortalExample';

export default function App() {
  return (
    <>
      <div className="clipping-container">
        <NoPortalExample  />
      </div>
      <div className="clipping-container">
        <PortalExample />
      </div>
    </>
  );
}

Pitfall

在使用 portal 时,确保你的应用具有可访问性非常重要。例如,你可能需要管理键盘焦点,以便用户能够自然地将焦点移入和移出 portal。

创建模态框时,请遵循 WAI-ARIA Modal Authoring Practices。如果你使用社区提供的包,请确保它具备可访问性并遵循这些指南。


将 React 组件渲染到非 React 的服务器标记中

如果你的 React 根节点只是一个静态页面或服务器渲染页面的一部分,而该页面并不是用 React 构建的,那么 portal 也会很有用。例如,如果你的页面是用 Rails 之类的服务器框架构建的,你可以在侧边栏等静态区域中创建交互区域。与拥有多个独立的 React 根节点相比,portal 让你可以将应用视为一个共享状态的单一 React 树,即使它的各个部分会渲染到 DOM 的不同位置。

import { createPortal } from 'react-dom';

const sidebarContentEl = document.getElementById('sidebar-content');

export default function App() {
  return (
    <>
      <MainContent />
      {createPortal(
        <SidebarContent />,
        sidebarContentEl
      )}
    </>
  );
}

function MainContent() {
  return <p>这一部分由 React 渲染</p>;
}

function SidebarContent() {
  return <p>这一部分也由 React 渲染!</p>;
}


将 React 组件渲染到非 React 的 DOM 节点中

你也可以使用 portal 来管理由 React 之外的系统管理的 DOM 节点中的内容。例如,假设你正在与一个非 React 的地图组件集成,并且想要在弹出窗口中渲染 React 内容。为此,声明一个 popupContainer 状态变量,用来保存你将要渲染到其中的 DOM 节点:

const [popupContainer, setPopupContainer] = useState(null);

当你创建第三方组件时,把该组件返回的 DOM 节点保存起来,这样你就可以向其中渲染内容:

useEffect(() => {
if (mapRef.current === null) {
const map = createMapWidget(containerRef.current);
mapRef.current = map;
const popupDiv = addPopupToMapWidget(map);
setPopupContainer(popupDiv);
}
}, []);

这让你可以在 popupContainer 可用后,使用 createPortal 将 React 内容渲染到其中:

return (
<div style={{ width: 250, height: 250 }} ref={containerRef}>
{popupContainer !== null && createPortal(
<p>Hello from React!</p>,
popupContainer
)}
</div>
);

下面是一个你可以实际尝试的完整示例:

import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createMapWidget, addPopupToMapWidget } from './map-widget.js';

export default function Map() {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const [popupContainer, setPopupContainer] = useState(null);

  useEffect(() => {
    if (mapRef.current === null) {
      const map = createMapWidget(containerRef.current);
      mapRef.current = map;
      const popupDiv = addPopupToMapWidget(map);
      setPopupContainer(popupDiv);
    }
  }, []);

  return (
    <div style={{ width: 250, height: 250 }} ref={containerRef}>
      {popupContainer !== null && createPortal(
        <p>Hello from React!</p>,
        popupContainer
      )}
    </div>
  );
}