簡介

React portal 是一個神奇的魔法。 官網的描述是這樣:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

簡單來說,它可以跨越 JSX 結構層級,依附在指定的地方。

以下是這個 portal 的 API 及所需參數:

ReactDOM.createPortal(child, container);

第一個參數 child:可以是 dom 元素、字串或是 fragment。(官網稱之為 renderable React child

第二個參數 container:要依附的目標(dom 元素)

使用時機

最常使用的時機,就是在使用 modal(dialog)元件的時候,因為它要凌駕所有的 dom 元素之上,才能覆蓋它們。 或父層元件擁有 overflow: hiddenz-index css 屬性,但是子元件想要不受父元件屬性限制的時候。

簡單範例

首先,我們將會有三個元件,分別是:Target、Parent、Child。 Child 元件是 Parent 元件的子元件,而 Target 元件則是我們要利用 React Portal 依附的目標元件。

Child 元件:設定寬、高皆為 400px

src/components/Child.jsx

export default function Child() {
  const STYLE = { width: 400, height: 400, background: "lightgreen" };

  return <div style={STYLE}>Child element</div>;
}

接著是 Parent 元件:設定寬、高為 300px,還有 overflow: hidden。 並將 Target 元件的參照,傳進 createPortal 的第二個參數裡,也就是依附對象。

src/components/Parent.jsx

import React from "react";
import ReactDOM from "react-dom";
import Child from "./Child";

function Parent(props) {
  const { forwardRef, targetEl } = props; // 將 target element 傳進來,供 createPortal 使用

  const STYLE = { width: 300, height: 300, background: "lightblue", overflow: "hidden" };

  return (
    <div ref={forwardRef} style={STYLE} onClick={handleClick}>
      Parent element
      {targetEl && ReactDOM.createPortal(<Child />, targetEl)}
    </div>
  );
}

export default React.forwardRef((props, ref) => <Parent forwardRef={ref} {...props} />);

另外,為了要取得該元件的元素參照,所以使用了 forwardRef

最後是 Target 元件:

src/components/Target.jsx

import React from "react";

function Target(props) {
  const { forwardRef } = props;

  const STYLE = { width: 500, height: 500, background: "lightgrey" };

  return (
    <div ref={forwardRef} style={STYLE}>
      Target element
    </div>
  );
}

export default React.forwardRef((props, ref) => <Target forwardRef={ref} {...props} />);

在 App 元件引用 Target、Parent 元件:

src/App.jsx

import { useState, useRef, useEffect } from "react";
import Target from "./components/Target";
import Parent from "./components/Parent";

function App() {
  const targetElRef = useRef();
  const parentElRef = useRef();
  const [currentTargetEl, setCurrentTargetEl] = useState(null);

  const STYLE = { display: "flex" };

  useEffect(() => {
    if (targetElRef.current) setCurrentTargetEl(targetElRef.current);
  }, []);

  return (
    <div>
      <div style={STYLE}>
        <Target forwardRef={targetElRef} />
        <Parent forwardRef={parentElRef} targetEl={currentTargetEl} />
      </div>
    </div>
  );
}

export default App;

特別注意的是,這裡在 useEffect 裡, 當 targetElRef 拿到 Target 元件的 dom 節點的時候, 更新 currentTargetEl 的值, 以便讓 Parent 元件能夠拿到 Target 元件的節點參照。 (由於 ref 的改變不會觸發 re-render,因此必須用 state 儲存起來)

讓我們來看一下結果:

從 developer tool 可以看出,Child 元件包覆在 Target 元件裡面,而不是如 JSX 的結構包覆在 Parent 元件裡。

利用 React Portal,Child 元件成功地跨越世界線逃脫 Parent 元件的掌控啦~

就跟奇異博士一樣,從砂輪機的火花圈中走出來

dr.strange

Portal 與事件冒泡(Event Bubbling)

React Portal 傳送門,將 element 傳送到我們想要的地方。 結果就是,dom 結構(DOM tree)改變了,但 JSX 的結構(React tree)卻不變。

也因此,Parent 元件這個 scope 裡面的所有東西(例如:props、function⋯⋯ 等),Child 元件仍然可以拿到。

所以「事件冒泡」當然也包含在內,我們在 Parent 元件裡,新增一個函式:handleClick

src/components/Parent.jsx

// ...

const handleClick = () => {
  console.log("parent click!!");
};

//...

並在根節點,當 onClick 事件觸發的時候呼叫它:

// ...

return (
  <div ref={forwardRef} style={STYLE} onClick={handleClick}>
    Parent element
    {targetEl && ReactDOM.createPortal(<Child />, targetEl)}
  </div>
);

然後我們會發現,無論是滑鼠點擊 Parent 元件,還是 Child 元件,都會印出 parent click!!! 然而點擊 Target 元件則毫無反應。

因此我們可以證明,Child 元件跑去寄宿在 Target 元件底下, 但同時又享有 Parent 元件資產的使用權(functionprops 等)

這不就是拿美國護照、用台灣健保的概念嗎??(喂

以上就是 React Portal 的簡單介紹~

上面的 demo code,請點選這個連結

我們下次見囉~ 👋