簡介
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: hidden
或 z-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 元件的掌控啦~
就跟奇異博士一樣,從砂輪機的火花圈中走出來
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 元件資產的使用權(function
、props
等)
這不就是拿美國護照、用台灣健保的概念嗎??(喂
以上就是 React Portal 的簡單介紹~
上面的 demo code,請點選這個連結
我們下次見囉~ 👋