1. ホーム
  2. React

【React】createPortal の使い方|モーダルを親のDOMの外に表示する方法

Share

モーダルやツールチップを作ると、「親要素の overflow: hidden で見切れてしまう」「z-index を大きくしても他の要素の下に隠れる」といった問題にぶつかることがあります。こうした「本来のDOMの位置に縛られたくない」要素を、別の場所に描画するための仕組みが createPortal です。この記事では、ポータルが必要になる理由、基本的な使い方、モーダルの実装例、そしてイベントの伝わり方まで、初心者向けに解説します。

ポータルが必要になる理由

React のコンポーネントは、書いた場所のDOMツリーの中に描画されます。ふだんはこれで問題ありませんが、モーダルのように「画面全体を覆いたい」要素では、親のスタイルが邪魔をすることがあります。たとえば親に overflow: hidden が付いていると、その外にはみ出した部分が切り取られてしまいます。z-index も、親が作る「重なりの文脈(スタッキングコンテキスト)」の中でしか効かないため、思ったように最前面に出せないことがあります。

createPortal を使うと、コンポーネントの見た目の描画先だけを、DOMツリーの別の場所(多くは body 直下)に移せます。これにより、親のスタイルの影響を受けずにモーダルを表示できます。

createPortal の基本

createPortal(children, domNode) は、第1引数に描画したい内容(JSX)、第2引数に描画先のDOM要素を渡します。返り値をそのまま JSX の中で返せば、指定した要素の中に描画されます。

portal-basic.tsx
import { createPortal } from "react-dom";

function Note() {
  // このコンポーネントを書いた場所ではなく、body 直下に描画される
  return createPortal(
    <p className="note">body の直下に表示されます</p>,
    document.body,
  );
}

createPortalreact-dom からインポートする点に注意してください(react ではありません)。この例では document.body を描画先にしていますが、あらかじめ HTML に用意した専用の要素(例:<div id="modal-root">)を指定することもよくあります。

モーダルを実装する

実際にモーダルを作ってみましょう。開閉の状態は useState で管理し、開いているときだけポータルでオーバーレイとダイアログを body 直下に描画します。

Modal.tsx
import { createPortal } from "react-dom";

type ModalProps = {
  open: boolean;
  onClose: () => void;
  children: React.ReactNode;
};

function Modal({ open, onClose, children }: ModalProps) {
  if (!open) return null; // 閉じているときは何も描画しない

  return createPortal(
    // オーバーレイ(背景)をクリックすると閉じる
    <div className="overlay" onClick={onClose}>
      {/* ダイアログ内のクリックは閉じないよう伝播を止める */}
      <div className="dialog" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>閉じる</button>
      </div>
    </div>,
    document.body,
  );
}
App.tsx
function App() {
  const [open, setOpen] = useState(false);

  return (
    <div style={{ overflow: "hidden" }}>
      <button onClick={() => setOpen(true)}>モーダルを開く</button>
      <Modal open={open} onClose={() => setOpen(false)}>
        <p>ここがモーダルの中身です。</p>
      </Modal>
    </div>
  );
}

親の divoverflow: hidden が付いていても、モーダルは body 直下に描画されるため、切り取られずに画面全体を覆えます。ポータルを使わずに同じ場所へ描画していたら、親のスタイルに引っかかってレイアウトが崩れていたはずです。

イベントはReactツリーを伝わる

ここが少し不思議に感じるところです。ポータルで描画された要素は、DOM上は body 直下にありますが、Reactのツリー上では元の場所にいるものとして扱われます。そのため、クリックなどのイベントは「書いた場所の親コンポーネント」へと伝播(バブリング)します。

つまり、ポータルの中で起きたクリックは、DOM上の body ではなく、JSX上で Modal を置いた場所の親がキャッチできます。この性質のおかげで、モーダルの中のボタンでも、親コンポーネントで用意したイベントハンドラーが自然に動作します。DOMの位置と React の論理的な位置がずれる、という点だけ覚えておきましょう。

まとめ

createPortal は、コンポーネントの描画先だけをDOMツリーの別の場所へ移す機能です。overflow: hiddenz-index といった親のスタイルに縛られたくないモーダル・ツールチップ・ドロップダウンなどで活躍します。react-dom からインポートし、createPortal(内容, 描画先) の形で使います。DOM上の位置は移動しますが、イベントはReactツリーに沿って伝わるため、親のイベントハンドラーはこれまで通り使えます。画面を覆うUIでレイアウトが崩れるときは、ポータルの導入を検討してみてください。

参考ページ