🙄

React Portalsの活用(Modal作り)

2021/03/03に公開

React PortalsはReactコンポーネントをレンダリングする時、UIをどこにレンダリングするかについてDOMを事前に選んで親コンポーネントの外側にもレンダリングすることができるようにする機能です。
v16で導入されてちょっと時間が経ちましたが(現在はv17)、なかなか使う機がなくて今回を機に頭に整理しながら使ってみたいと思います。

基本

今まではchildrenは親コンポーネントのDOM内部にレンダリングする必要がありました。

const Wrapper = ({ children }) => {
  return <div>{children}</div>;
}

Portalsを使ったら、DOM階層構造に関わらずコンポーネントをレンダリングすることができます。

const Portal = ({ children }) => {
  const el = document.getElementById('portal');
  return ReactDOM.createPortal(children, el);
}

どこかで見た気がするコードですね。はいエントリーファイル(src/index.js)を見ると以下のコードの確認ができます。

ReactDOM.render(<App />, document.getElementById('root'));

document.getElementByIdを使って、idがrootのDOMを探して、Appコンポーネントにレンダリングするコードです。そのDOMはpublic/index.htmlから確認できます。
public/index.html

<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="root"></div>
</body>

活用

Portalsの活用ができる良い例としてはModal(モーダル)があります。その理由に関しては後で説明します。それではさっそく作ってみます。
まず、ModalのためのModalPortalコンポーネントを作成します。
public/index.html

<body>
  (...省略)
  <div id="root"></div>
  <div id="modal"></div>
</body>

src/ModalPortal.js

import ReactDOM from 'react-dom';

const ModalPortal = ({ children }) => {
  const el = document.getElementById('modal');
  return ReactDOM.createPortal(children, el);
};

export default ModalPortal;

次にModalコンポーネントを作成します。
src/Modal.css

.modal {
  background: rgba(0, 0, 0, 0.25);
  position: fixed;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal .content {
  background: white;
  padding: 1rem;
  width: 400px;
  height: auto;
}

src/Modal.js

import './Modal.css';

const Modal = () => {
  return (
    <div className="modal">
      <div className="content">
        <h3>Modal</h3>
        <p>This is Content</p>
        <button>Close</button>
      </div>
    </div>
  );
};

export default Modal;

src/App.css

.App {
  text-align: center;
  color: #61dafb;
}

src/App.js

import "./App.css";
import Modal from "./Modal";

export default function App() {
  return (
    <div className="App">
      <h1>Hello World</h1>
      <Modal />
    </div>
  );
}


ModalコンポーネントがAppコンポーネントの内部にレンダリングされて、Appコンポーネントのスタイルが適用されたのが確認できます。
続いて、ModalPortalコンポーネントでModalコンポーネントを包み込んでみます。

src/App.js

import "./App.css";
import Modal from "./Modal";
import ModalPortal from "./ModalPortal";

export default function App() {
  return (
    <div className="App">
      <h1>Hello World</h1>
      <ModalPortal>
        <Modal />
      </ModalPortal>
    </div>
  );
}


ModalコンポーネントがAppコンポーネントの外側(<div id="modal"></div>)にレンダリングされ、Appコンポーネントのスタイルが解除されたのが確認できます。
このようにPortalを使ったら、不要なスタイルが適用されてしまう問題の対応やエレメントのレイヤー位置を決めるz-index等のスタイルを管理する時に役に立ちます。

終わりに

Portalsを使ったら、希望するDOMにレンダリングすることができます。Appコンポーネント以外にも構いません。Modalのようなコンポーネントを作る時、スタイルに関わる問題にぶつかったらPortalsを考慮してみることも良いと思います。

Discussion