關於 React 傳送門 (Portal)
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)
Section titled “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,請點選這個連結
我們下次見囉~ 👋