Portal
Portal 提供一個優秀方法來讓 children 可以 render 到 parent component DOM 樹以外的 DOM 節點。
ReactDOM.createPortal(child, container)
第一個參數(child
)是任何可 render 的 React child,例如 element、string 或者 fragment。第二個參數(container
)則是一個 DOM element。
使用方式
通常當 component 的 render 方法回傳一個 element 時,此 element 會作為 child 被 mount 進最接近的 parent 節點中:
render() {
// React mount 一個新的 div 並將 children render 進去
return (
<div> {this.props.children}
</div> );
}
然而在某些狀況下,將 child 插入不同位置的 DOM 內十分好用:
render() {
// React *不會*建立新的 div。它會將 children render 進 `domNode` 中。
// `domNode` 可以是任何在隨意位置的合法 DOM node。
return ReactDOM.createPortal(
this.props.children,
domNode );
}
一個典型的 portal 使用案例是,當 parent component 有 overflow: hidden
或者 z-index
的樣式時,卻仍需要 child 在視覺上「跳出」其容器的狀況。例如 dialog、hovercard 與 tooltip 都屬於此案例。
注意:
當使用 portal 時,請留意控管鍵盤 focus 對於無障礙功能會變得非常重要。
使用跳窗 dialog 時,應確保每個人都可以依照 WAI-ARIA Modal 開發規範 定義的方式與其互動。
透過 Portal 進行 Event Bubbling
雖然 portal 可以被放置在 DOM tree 中的任何位置,但 portal 的其他行為與一般的 React child 別無二致。像是 context 等功能的運作方式並不會因為 child 是 portal 而有所不同,因為不論 portal 在 DOM tree 中的位置為何,它都存在於 React tree 中。
相同的行為也包括 event bubbling。一個在 portal 內觸發的 event 會傳遞到涵蓋它的 React tree 祖先中,就算涵蓋它的那些 element 並不是 DOM tree 上的祖先。假設存在以下 HTML 結構:
<html>
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html>
一個在 #app-root
中的 Parent
component 可以捕捉從 sibling 節點 #modal-root
bubble 上來且未被接收過的 event。
// 這兩個 container 是 DOM 上的 sibling
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
// Portal element 會在 Modal 的 children 被
// mount 之後才插入 DOM tree 中,這代表 children
// 會被 mount 在一個分離的 DOM 節點上。如果一個
// child component 需要在 mount 結束時馬上連接到 DOM tree 中,
// 例如測量一個 DOM node,或者在子節點中使用 'autoFocus' 等狀況,
// 則應將 state 加入 Modal 中,並只在 Modal 插入 DOM tree 後
// 才 render children。
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children, this.el ); }}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 這會在 Child 中的 button 被點擊時觸發並更新 Parent 的 state, // 就算 Child 的 button 並不是 DOM 中的直接後代。 this.setState(state => ({ clicks: state.clicks + 1 })); } render() { return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p> Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal> </div> ); }
}
function Child() {
// 這個 button 中的 click event 會被 bubble 到 parent 中,
// 因為這邊並沒有定義 'onClick' attribute
return ( <div className="modal"> <button>Click</button>
</div>
);}
const root = ReactDOM.createRoot(appRoot);
root.render(<Parent />);
Parent component 可以捕捉從 portal bubble 上來的 event 能使開發具有不直接依賴於 portal 的更彈性化抽象性。舉例來說,如果你 render 了一個 <Modal />
component,則不論 Modal 是否是使用 portal 實作,它的 parent 皆可以捕捉到其 event。