React Portal组件

Posted on Aug 9, 2023

概念

createPortal函数用于将组件渲染到DOM的不同位置,即”脱离“父组件的DOM。与react组件的渲染不同,createPortal需要指定一个DOM节点去渲染。

createPortal函数

createPortal(children, domNode, key?) 

参数

  • children: 可以被react渲染的任意组件。
  • domNode: DOM节点,这个节点必须是一件存在的,如果在组件更新过程中,使用了一个不同的节点,会导致portal的重新创建。
  • key: 可选,portal的唯一表示

调用方式,下面这段代码会在document.body元素中渲染出:<p>This child is placed in the document body.</p>.

import { createPortal } from 'react-dom';

...

<div>
  <p>This child is placed in the parent div.</p>
  {createPortal(
    <p>This child is placed in the document body.</p>,
    document.body
  )}
</div>

使用场景

createPortal的使用场景通常是将一个组件渲染到DOM的不同位置,如模态框、全局提示组件等。

常见的的使用场景:

  1. 模态框。
    • 模态对话框(Modal Dialogue Box,又叫做模式对话框),是指在用户想要对对话框以外的应用程序进行操作时,必须首先对该对话框进行响应。如单击【确定】或【取消】按钮等将该对话框关闭。
  2. 将React组件渲染到非React管理的DOM中。

下面展示了一个使用createPortal函数将Footer组件渲染到#footer div容器的示例。如下图,黑色边框内的元素是React组件(<App />),红色边框的<Footer />是react之外的元素,不受react管理,绿色边框内的元素是由react渲染的。

image 查看demo CodeSandbox

代码实现:

  • index.html,在这段代码中有两个div元素:#root#footer。其中,#root是react组件将会挂载的容器,而#footer是react portal组件的容器。
<body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <div
      id="footer"
      style="border: 1px solid red; padding: 10px; margin-top: 20px;"
    >
      <ul>
        Footer
      </ul>
    </div>
</body>
  • App.js,在这个文件中,声明了一个React组件<App />,这个组件会被挂载到#root容器中,在下面的代码中使用了createPortal函数,将<Footer />组件渲染到了#footer容器。
import { createPortal } from "react-dom";
import Footer from "./Footer";
import "./styles.css";

export default function App() {
  return (
    <div className="App" style={{ border: "1px solid #333" }}>
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      {createPortal(
        <Footer>
          <div style={{ border: "1px solid green" }}>
            <div>由react portal渲染</div>
            <ol>
              {[1, 2, 3, 4, 5].map((it) => {
                return <li key={it}>item{it}</li>;
              })}
            </ol>
          </div>
        </Footer>,
        document.querySelector("#footer")
      )}
    </div>
  );
}
  • Footer.js,这个文件声明了<Footer />组件,它将children渲染出来。
import React from "react";

export default function Footer({ children }) {
  return (
    <div>
      <div>{'<Footer>'}</div>
      {children}
      <div>{'</Footer>'}</div>
    </div>
  );
}

事件处理

portal组件的事件处理与其他react组件是一致的。如果在portal组件中点击,在父组件也是会捕获到的,即使它们不在同一个DOM层级中。

在下图中,展示了一个计数器,当点击App组件时计数器加1。当点击portal组件时,计数器也会加1。

image

代码如下:

import { useState } from "react";
import { createPortal } from "react-dom";
import Footer from "./Footer";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  const handleCountIncrease = e => {
    setCount(count+1);
    console.log(e);
  }

  return (
    <div onClick={handleCountIncrease} className="App" style={{ border: "1px solid #333" }}>
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <div>Count: {count}</div>
      {createPortal(
        <Footer>
          <div style={{ border: "1px solid green" }}>
            <div>由react portal渲染</div>
            <ol>
              {[1, 2, 3, 4, 5].map((it) => {
                return <li key={it}>item{it}</li>;
              })}
            </ol>
          </div>
        </Footer>,
        document.querySelector("#footer")
      )}
    </div>
  );
}

与ReactDOM.render的不同

ReactDOM.render函数是将React组件挂载到DOM一个节点中,createPortal也是将组件渲染到DOM节点。 不同的是ReactDOM.render函数会为每个组件创建一个上下文,即这些组件不能共享状态、事件。 createPortal仅仅是将组件渲染到别的DOM节点,其他的如状态、事件处理都是不变。

参考

  1. https://react.dev/reference/react-dom/createPortal
  2. https://baike.baidu.com/item/%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/6449704