フロントエンドの最前線で戦う諸君、お疲れ様。
Reactを使い込んでくると、「コンポーネントツリーの階層」と「DOMツリーの階層」が必ずしも一致しないほうが都合がいいケースに必ずぶつかるはずだ。モーダル、ツールチップ、あるいはドロップダウンメニュー。これらはReactのコンポーネント構造上は親の中にありながら、CSSの`z-index`や`overflow: hidden`、あるいは`position: relative`の制約を無視して、DOMの「一番外側」に鎮座していてほしい。
そこで登場するのが `createPortal` だ。今日は、単なる「便利なAPI」という理解を超えて、ブラウザの裏側で何が起きているのか、そして現場でどう使いこなすべきか、プロの視点で深掘りしていくぞ。
—
1. なぜ「物理的な階層」を飛び出す必要があるのか?
Reactのコンポーネントは本来、親から子へ、綺麗にツリー状にレンダリングされる。しかし、モーダルを作る場面を想像してほしい。
親コンポーネントに `overflow: hidden` が設定されていたらどうなる? モーダルは親の領域をはみ出せず、無残に切り取られてしまう。あるいは、親に `z-index` のコンテキストが形成されていれば、モーダルがどんなに高い `z-index` を持とうと、その親の囲いから抜け出せない。
`createPortal` は、「Reactのコンポーネントとしては親の管理下にある(つまりStateやContextは共有できる)が、DOMの配置場所だけは物理的に別の場所に飛ばす」という、魔法のような挙動を実現するんだ。
—
2. 現場で即戦力となる「汎用モーダル」の実装
口で説明するより、コードを見せたほうが早いな。これが、僕が実務でよく使う、React 18以降のクリーンなPortal実装パターンだ。
import { useEffect, useState, ReactNode } from ‘react’;
import { createPortal } from ‘react-dom’;
type ModalProps = {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
};
export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
const [container] = useState(() => document.createElement(‘div’));
useEffect(() => {
// 実際にDOMへ追加する処理
const modalRoot = document.getElementById(‘modal-root’) || document.body;
modalRoot.appendChild(container);
// クリーンアップ処理:モーダルが消えたらDOMも削除する
return () => {
modalRoot.removeChild(container);
};
}, [container]);
if (!isOpen) return null;
// Reactの力を借りて、作成したdiv内にレンダリングする
return createPortal(
{children}
,
container
);
};
このコードの「プロのこだわり」
1. コンテナの動的生成: `useState` の初期値にコールバックを渡すことで、コンポーネントがマウントされる瞬間に一度だけ `div` を作成する。余計な再レンダリングを防ぐ鉄則だ。
2. クリーンアップの徹底: `useEffect` の戻り値で `removeChild` を呼ぶのは必須だ。これを忘れると、モーダルを開閉するたびにゴミDOMが溜まっていく。メモリリークの温床になるから注意してくれ。
3. イベントバブリングの罠: `stopPropagation()` を使っているのは、モーダルの背景をクリックしたときだけ閉じるようにするためだ。Portalを使っても、Reactのイベントバブリングは「コンポーネントツリー」を基準に動く。この仕様を知っているかどうかが、シニアへの分かれ道だ。
—
3. ブラウザ視点での「裏側の挙動」を理解する
多くのエンジニアが勘違いしているが、Portalを使ってもReactの仮想DOMツリーには影響を与えない。
ブラウザの「要素の検証(DevTools)」で見ると、モーダルが `
` 直下に表示されているように見えるだろう。しかし、Reactの内部的には、そのモーダルは依然として呼び出し元のコンポーネントの子として扱われている。これが意味するのは、「Portal越しでもContextやReduxのStoreはそのまま参照できる」ということだ。もしPortalが単なる「別のページへの挿入」だったら、認証情報やテーマのContextが引き継げずに苦労するはずだが、Reactはそこを天才的な設計で解決している。
—
4. 現場でのTips:実装の落とし穴
最後に、現場で泣きを見ないためのアドバイスを二つ。
- `modal-root` を忘れるな: `document.body` に直接突っ込むのもアリだが、プロジェクトの `index.html` に `
` を定義しておき、そこに挿入するのがベストプラクティスだ。他のスクリプトとの競合を避けるための「名前空間」のようなものだと思ってくれ。
- アクセシビリティ(A11y)の意識: PortalでDOMを飛ばしても、フォーカスの管理は物理階層に依存する。モーダルが開いたとき、フォーカスをモーダル内に閉じ込める(Focus Trap)処理を忘れると、スクリーンリーダー利用者は迷子になる。`react-focus-lock` のようなライブラリの力を借りるか、自前でフォーカス制御を実装する準備はしておこう。
—
まとめ
`createPortal` は、Reactの「コンポーネント思考」を壊さずに、ブラウザの「DOMの制約」を突破するための最強の武器だ。
「見た目」と「ロジック」の結合度を適切に分離し、DOMを自由に操れるようになれば、君が作るUIは一気にプロフェッショナルなレベルへと押し上げられるはずだ。
次は、このPortalを活用した「ツールチップの自動位置調整」について深掘りしてもいい。今の実装に満足せず、常に「裏側で何が起きているか」を想像しながらコードを書いてみてくれ。また現場で会おう。

コメント