Portalsはコンポーネントを記述したその場所に表示させるのではなく事前に指定した別の場所にコンポーネントの内容を表示させることができる機能です。

React.jsのドキュメントではPortalsは次のように説明されています。”Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.”(親コンポーネントのDOM階層の外側にあるDOMノードの中に子(要素)を描写するためのファーストクラスの方法を提供する。)

日本語の訳のわかりにくさを含めて上記の文章でピンとこなくてもこれから利用するシンプルな例を使って動作確認を行うと上記の説明とPortalsの基本的な利用方法を理解することができます。Portalsの基本を理解した後にPortalsを利用してシンプルなモーダルウィンドウの作成も行います。

なぜPortalsを利用するのか

コンポーネントの組み合わせにより複雑に要素が重なり合った状態ではz-indexを設定したにも関わらず期待通りに表示されない、また複数のコンポーネントが別々のモーダルウィンドウを持っている場合にコンポーネント毎にモーダルウィンドウの設定が必要なので手間がかかるといった経験はないですか?

この問題を解決するため、コンポーネント毎で個別に要素の重なりを調整していくのではなく”ここにモーダルウィンドウの要素をおけば要素の重なりを意識することなくモーダルウィンドウを表示できますよ”という共有の場所をPortalsの機能を利用して設定します。その共有の場所が設定できたら、各コンポーネントの中でその共有場所に表示させたい要素(モーダルウィンドウの要素)に対して設定した共有場所を指定します。その結果、各コンポーネントのモーダルウィンドは要素の重なりの調整から解放され、作成した場所ではなく指定した共有の場所でモーダルウィンドウの要素を表示させることができるようになります。

初めてのPortalsの設定

動作確認を行う前にnpx create-react-appコマンドでReactのプロジェクトの作成を行なってください。

Portalsを設定するためには大きく2つの設定を行います。一つは要素を表示する場所の設定、もう一つは表示させたい内容の設定(Portalsの作成)です。

最初にPortalsで作成する要素を表示したい場所の指定を行います。index.htmlファイルの中にidの値にportalを持つdiv要素を追加します(idに設定する名前は任意です)。この場所に後ほど設定するPortalsの内容が表示されることになります。


<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <!--
    This HTML file is a template.
    If you open it directly in the browser, you will see an empty page.

    You can add webfonts, meta tags, or analytics to this file.
    The build step will place the bundled scripts into the <body> tag.

    To begin the development, run `npm start` or `yarn start`.
    To create a production bundle, use `npm run build` or `yarn build`.
  -->
  <div id="portal"></div>
</body>

通常Reactのコードの内容は<div id=”root”></div>にマウントが行わるためこの要素の内側に表示されます。しかし今回は<div id=”root”></div>とは異なる<div id=”portal”></div>を設定しています。<div id=”root”></div>階層とは異なる<div id=”portal”></div>に描写することは、本書の冒頭に出てきたPortalsのドキュメントの説明にある”親コンポーネントのDOM階層の外側にあるDOMノードの中に子(要素)を描写する”という意味に対応します。

次に表示させたい内容の設定を行っていきます。App.jsファイルに下記を記述します。


import ReactDOM from 'react-dom';

const Test = () => {
  return ReactDOM.createPortal(
    <h1>Hello Portals</h1>,
    document.getElementById('portal')
  );
};

function App() {
  return (
    <div>
      <Test />
      <p>初めてのreact.jsでのPortalsの設定です。</p>
    </div>
  );
}

export default App;

Testコンポーネントの中でPortalsの作成(設定)を行っています。PortalsはReactDomのcreatePortalメソッドで作成することができ、第一引数には表示させたい内容(child)、第二引数には先ほどindex.htmlで追加したidにportalを持つdiv要素(container)をdocument.getElementByIdメソッドで取得しています。


ReactDOM.createPortal(child, container)

Testコンポーネントを作成したらAppコンポーネントの中にTestコンポーネントを追加します。これだけの作業でPortalsの設定は完了です。

Testコンポーネントは別ファイルでTest.jsファイルを作成してApp.jsファイルでimportして利用しても問題ありません。

ブラウザで確認すると下記のように表示されます。通常ではTestコンポーネントはpタグの”初めてのreact.jsでのPortalsの設定です。”よりも上に記述しているためHello Portalsの文字列の方が先に表示されると思われますがPortalsの設定を行なっていることによりTestコンポーネントを記述した場所ではなくidにportalを持つdiv要素の場所に表示されています。

初めてのPortalsの設定
初めてのPortalsの設定

画面を見ただけではわかりにくいですが、ブラウザのデベロッパーツールで要素の確認を行なってください。idにportalを持つdiv要素の中に”Hello Portals”が入っていることがわかります。<div id=”root”></div>階層とは異なる場所で描写されていることが下記の画面から理解することができます。

デベロッパーツールでのPortalsの確認
デベロッパーツールでのPortalsの確認

ここまでの動作確認で本文書の最初に記述していた”Portalsを使うことでコンポーネントを記述したその場所に表示させるのではなく事前に指定した別の場所に表示させることができます。”と”親コンポーネントのDOM階層の外側にあるDOMノードの中に子(要素)を描写するためのファーストクラスの方法を提供する。”いう意味が理解できたのではないでしょうか。

Testコンポーネントの中に直接記述するのではなくpropsのchildrenを使うことも可能です。


import ReactDOM from 'react-dom';

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

function App() {
  return (
    <div>
      <Test>
        <h1>Hello Portals</h1>
      </Test>
      <p>初めてのreact.jsでのPortalsの設定です。</p>
    </div>
  );
}

export default App;

Portalsと表示・非表示設定

Portalsはモーダル・ダイアログなどでも利用することができます。モーダルで利用する際はコンポーネントの表示・非表示を切り替えることになるので表示・非表示を切り替えの動作確認を行っておきます。

useStateでshow変数を追加します。show変数をtrue, falseに切り替えることでTestコンポーネントの表示・非表示を切り替えています。


import ReactDOM from 'react-dom';
import { useState } from 'react';

const Test = () => {
  return ReactDOM.createPortal(
    <h1>Hello Portals</h1>,
    document.getElementById('portal')
  );
};

function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ margin: '2em' }}>
      {show && <Test />}
      <div>
        <button onClick={() => setShow(!show)}>Toggle</button>
      </div>

      <p>初めてのreact.jsでのPortalsの設定です。</p>
    </div>
  );
}

export default App;

表示直後の状態ではPortalsの内容は表示されていません。

PortalとuseStateの組み合わせ
PortalとuseStateの組み合わせ

ToggleボタンをクリックするとPortalsの内容が表示されます。

Toggleボタンをクリックで表示
Toggleボタンをクリックで表示

Toggleボタンをクリックすることで表示・非表示を切り替えることができます。

DOM要素の設定場所

createPortalsを実行する際に指定したdiv id=”portal”はindex.htmlファイルに記述しましたが別の場所に記述した場合でも動作するのか確認しておきます。

index.htmlファイルから削除を行い、App.jsファイルに追加します。


function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ margin: '2em' }}>
      {show && <Test />}
      <div>
        <button onClick={() => setShow(!show)}>Toggle</button>
      </div>

      <p>初めてのreact.jsでのPortalsの設定です。</p>
      <div id="portal"></div>
    </div>
  );
}

index.htmlファイルからApp.jsファイルへ記述場所を変えても先ほどと同様にPortalsの内容の表示・非表示の切り替えは可能です。

次にshowのデフォルトの値をfalseからtrueに変更してください。


const [show, setShow] = useState(true);

showのデフォルト値を変更するとブラウザ上には、”Error: Target container is not a DOM element.”のエラーが表示されます。

Error: Target container is not a DOM element.
Error: Target container is not a DOM element.

showをtrueにするとTestコンポーネントがブラウザ上に描写させるためReactDOM.createPortalを実行します。しかしReactDOM.createPortalを実行時にはまだ<div id=”portal”>が準備されていないためdocument.getElementById(‘portal’)で要素が取得できません。そのことが原因でエラーが表示されています。

useEffectを利用して<div id=”portal”>が準備されているかどうかの分岐を入れることでこの問題は回避することができます。


import ReactDOM from 'react-dom';
import { useState, useEffect } from 'react';

const Test = () => {
  return ReactDOM.createPortal(
    <h1>Hello Portals</h1>,
    document.getElementById('portal')
  );
};

function App() {
  const [show, setShow] = useState(true);
  const [domReady, setDomReady] = useState(false);
  useEffect(() => {
    setDomReady(true);
  });
  return (
    <div style={{ margin: '2em' }}>
      {show && domReady && <Test />}
      <div>
        <button onClick={() => setShow(!show)}>Toggle</button>
      </div>

      <p>初めてのreact.jsでのPortalsの設定です。</p>
      <div id="portal"></div>
    </div>
  );
}

export default App;

index.htmlファイルに<div id=”portal”>設定している場合は常に要素は存在しているので”Error: Target container is not a DOM element.”のエラーが表示されることはありません。

モーダルウィンドウの作成

ここまででPortalsの基礎を理解することができたのでPortalsを利用して簡易的なモーダルウィンドウを作成します。

Modal.jsファイルを新たに作成して以下の内容を記述してください。ReactDomのcreatePortalメソッドの第一引数のchildの記述が先ほどよりも長くなっていますがcreatePortalメソッドの書式に従って記述しています。


import ReactDOM from 'react-dom';

const Modal = ({ children, closeModal }) => {
  return ReactDOM.createPortal(
    <div
      style={{
        position: 'fixed',
        top: '0',
        left: '0',
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'rgba(0,0,0,0.5)',
      }}
    >
      <div style={{ width: '50%', padding: '1em', backgroundColor: 'white' }}>
        <div>{children}</div>
        <div>
          <button onClick={closeModal}>Close</button>
        </div>
      </div>
    </div>,
    document.getElementById('portal')
  );
};

export default Modal;

一見複雑そうに見えるかもしれませんが大半が表示されるモーダルウィンドウを画面一杯に表示させ、propsで渡させるコンテンツを中央に表示させるstyle属性の設定です。

propsでは親コンポーネントから渡されるchildrenとモーダルウィンドウを閉じるためのcloseModalメソッドを受け取っています。

App.jsファイルでは作成したModalコンポーネントをimportして新たにModalコンポーネントに渡すclsoeModalメソッドを追加しています。ボタンの名前もtoggleからopenに変更しています。


import { useState } from 'react';
import Modal from './Modal';

function App() {
  const [show, setShow] = useState(false);
  const closeModal = () => {
    setShow(false);
  };
  return (
    <div style={{ margin: '2em' }}>
      {show && <Modal closeModal={closeModal}>モーダルウィンドウの作成</Modal>}
      <div>
        <button onClick={() => setShow(!show)}>Open</button>
      </div>
      <p>初めてのreact.jsでのPortalsの設定です。</p>
    </div>
  );
}

export default App;

これで簡易的なモーダルウィンドウの作成は完了です。デフォルトではshow変数がfalseなのでモーダルウィンドウは表示されていません。Openボタンをクリックします。

モーダルが非表示の状態
モーダルが非表示の状態

背景がグレーの透過50%で表示され、その中心にコンテンツが表示されます。Closeボタンをクリックするとモーダルウィンドウは非表示となります。

モーダルウィンドウが画面上に表示
モーダルウィンドウが画面上に表示

シンプルなコードを使った例でしたがPortalsの基本を理解できたのではないでしょうか。実際にこのPortalsの考えを利用したモーダルやダイアログはさまざまな場所で利用されているので要素の重なりに困った場合などにぜひチャレンジしてみてください。