モーダルやツールチップを作ると、「親要素の overflow: hidden で見切れてしまう」「z-index を大きくしても他の要素の下に隠れる」といった問題にぶつかることがあります。こうした「本来のDOMの位置に縛られたくない」要素を、別の場所に描画するための仕組みが createPortal です。この記事では、ポータルが必要になる理由、基本的な使い方、モーダルの実装例、そしてイベントの伝わり方まで、初心者向けに解説します。
目次
ポータルが必要になる理由
React のコンポーネントは、書いた場所のDOMツリーの中に描画されます。ふだんはこれで問題ありませんが、モーダルのように「画面全体を覆いたい」要素では、親のスタイルが邪魔をすることがあります。たとえば親に overflow: hidden が付いていると、その外にはみ出した部分が切り取られてしまいます。z-index も、親が作る「重なりの文脈(スタッキングコンテキスト)」の中でしか効かないため、思ったように最前面に出せないことがあります。
createPortal を使うと、コンポーネントの見た目の描画先だけを、DOMツリーの別の場所(多くは body 直下)に移せます。これにより、親のスタイルの影響を受けずにモーダルを表示できます。
createPortal の基本
createPortal(children, domNode) は、第1引数に描画したい内容(JSX)、第2引数に描画先のDOM要素を渡します。返り値をそのまま JSX の中で返せば、指定した要素の中に描画されます。
import { createPortal } from "react-dom";
function Note() {
// このコンポーネントを書いた場所ではなく、body 直下に描画される
return createPortal(
<p className="note">body の直下に表示されます</p>,
document.body,
);
}
createPortal は react-dom からインポートする点に注意してください(react ではありません)。この例では document.body を描画先にしていますが、あらかじめ HTML に用意した専用の要素(例:<div id="modal-root">)を指定することもよくあります。
モーダルを実装する
実際にモーダルを作ってみましょう。開閉の状態は useState で管理し、開いているときだけポータルでオーバーレイとダイアログを body 直下に描画します。
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,
);
}
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>
);
}
親の div に overflow: hidden が付いていても、モーダルは body 直下に描画されるため、切り取られずに画面全体を覆えます。ポータルを使わずに同じ場所へ描画していたら、親のスタイルに引っかかってレイアウトが崩れていたはずです。
イベントはReactツリーを伝わる
ここが少し不思議に感じるところです。ポータルで描画された要素は、DOM上は body 直下にありますが、Reactのツリー上では元の場所にいるものとして扱われます。そのため、クリックなどのイベントは「書いた場所の親コンポーネント」へと伝播(バブリング)します。
つまり、ポータルの中で起きたクリックは、DOM上の body ではなく、JSX上で Modal を置いた場所の親がキャッチできます。この性質のおかげで、モーダルの中のボタンでも、親コンポーネントで用意したイベントハンドラーが自然に動作します。DOMの位置と React の論理的な位置がずれる、という点だけ覚えておきましょう。
まとめ
createPortal は、コンポーネントの描画先だけをDOMツリーの別の場所へ移す機能です。overflow: hidden や z-index といった親のスタイルに縛られたくないモーダル・ツールチップ・ドロップダウンなどで活躍します。react-dom からインポートし、createPortal(内容, 描画先) の形で使います。DOM上の位置は移動しますが、イベントはReactツリーに沿って伝わるため、親のイベントハンドラーはこれまで通り使えます。画面を覆うUIでレイアウトが崩れるときは、ポータルの導入を検討してみてください。