React Portal

Portal 是從 React 16 開始提供的功能,用來將元件渲染到父元件之外的 DOM 節點上。可以將 Portal 想像成,你 render 一個元件時,其實是改變頁面上某個地方的 DOM 結構。

你可能會想,那 Portal 是用在什麼情況?通常的 React 元件,你只能渲染在父元件下面,但如果你要寫一個 modal 對話框,或說父元件寫死了 overflow: hidden 或你遇到 z-index 的問題呢?這就是使用 Portal 的時候了!

你可以在 render() 中使用 Portal:

ReactDOM.createPortal(child, container)

其中第一個參數 child 是你要渲染的 React 元素,第二個參數 container 則是要被掛載到哪一個 DOM 元素上。

例如頁面中的 HTML 結構如果是:

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

你可以這樣子寫一個 modal:

// DOM 中的兩個兄弟元素 app-root 和 modal-root
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

// 一個 modal 元件
class Modal extends React.Component {
  constructor(props) {
    super(props);
    // 因為我們想要每個 modal 都是獨立分開的
    // 所以每個 modal 都建立一個自己的 DOM container
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // 元件 mount 時,將 container 放到 #modal-root 元素中
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    // 元件 unmount 時,移除 container
    modalRoot.removeChild(this.el);
  }

  render() {
    // 運用 Portal 功能,將 React 元素 render 到指定的 DOM element
    return ReactDOM.createPortal(
      this.props.children,
      // 第二個參數指定元件要掛載到這個 DOM element
      this.el,
    );
  }
}

// 接著,像一般元件的使用,我們可以在任意元件中使用 Modal 元件
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showModal: false};

    this.handleShow = this.handleShow.bind(this);
    this.handleHide = this.handleHide.bind(this);
  }

  handleShow() {
    this.setState({showModal: true});
  }

  handleHide() {
    this.setState({showModal: false});
  }

  render() {
    // 點按鈕來出現 modal
    const modal = this.state.showModal ? (
      <Modal>
        <div className="modal">
          我是被渲染到 #modal-container 的 modal
          <button onClick={this.handleHide}>關掉 modal</button>
        </div>
      </Modal>
    ) : null;

    return (
      <div className="app">
        <button onClick={this.handleShow}>打開 modal</button>
        {modal}
      </div>
    );
  }
}

ReactDOM.render(<App />, appRoot);

點我看這個例子的結果

Portal 的事件冒泡 (Event Bubbling)

雖然 Portal 可能會被掛載到 DOM 中任何的地方,但使用上和一般的 React 元件都是一樣的,React 底層都幫你處理好了!

這包含事件的處理,像是上面的例子中的 HTML 結構:

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

在父元件 #app-root 裡面,是能夠捕抓到 #modal-root 冒泡上來的事件喔!(在 DOM 結構中,因為 #modal-root 是 #app-root 的兄弟節點,不是 #app-root 的子節點,理論上 #app-root 是收不到 #modal-root 的事件的)