React Portals:DOMの「物理的な制約」を華麗にハックする技術
やあ。今日もフロントエンドの深淵を覗いているかい?
Reactを触り始めてしばらくすると、誰もが一度はぶつかる壁がある。「CSSの`overflow: hidden`や`z-index`の呪縛」だ。モーダルを作ろうとして、親要素の`overflow: hidden`に遮られて表示が欠けたり、重なり順の地獄(z-index hell)に陥ったりした経験は、中級者なら一度はあるはずだ。
「Reactのコンポーネント設計的にはこの場所に置きたい。でも、DOMの階層としてはもっと外側に置かないとCSSが効かない……」。このジレンマを解消する唯一にして最強の武器が、`ReactDOM.createPortal` だ。
今回は、単なるAPIの紹介で終わらせず、ブラウザの裏側で何が起きているのか、そして実務でどう使いこなすべきか、現場の視点から紐解いていこう。
—
1. なぜ「物理的なDOM階層」を分離する必要があるのか?
Reactの基本思想は「コンポーネントのツリー構造とDOMのツリー構造を一致させること」だ。しかし、WebアプリケーションのUIは必ずしもこのツリー構造と一致しない。
例えば、`Header`コンポーネントの中に配置した「通知モーダル」を考えてみてほしい。
- React的な構造: `Header`の中にある。
- UI的な要求: 画面全体を覆い、他のどの要素よりも手前に表示したい。
もしReactのツリー通りに`Header`の内部にモーダルをレンダリングすると、`Header`に`position: relative`や`overflow: hidden`がかかっている場合、モーダルはそこで切り取られてしまう。
ここで`createPortal`の出番だ。これは、「Reactのコンポーネントツリーの階層を維持したまま、DOMのノードだけをHTMLの別の場所に移動させる」という魔法をかける。
—
2. 実践:再利用可能なPortalコンポーネント
現場で「とりあえず動く」コードを書くのではなく、疎結合でテストしやすいPortalコンポーネントを作ろう。ポイントは、マウント先のDOMノードが存在しない場合のハンドリングだ。
import { useEffect, useState } from ‘react’;
import { createPortal } from ‘react-dom’;
interface PortalProps {
children: React.ReactNode;
containerId?: string; // 任意のターゲット要素ID
}
export const Portal = ({ children, containerId = ‘portal-root’ }: PortalProps) => {
const [container, setContainer] = useState
useEffect(() => {
// ターゲットとなるDOM要素を取得、なければ生成する
let element = document.getElementById(containerId);
if (!element) {
element = document.createElement(‘div’);
element.setAttribute(‘id’, containerId);
document.body.appendChild(element);
}
setContainer(element);
// クリーンアップ処理: コンポーネントが消えたらDOMも掃除する
return () => {
if (element?.childNodes.length === 0) {
element.remove();
}
};
}, [containerId]);
// containerが準備できるまでレンダリングを待機
if (!container) return null;
return createPortal(children, container);
};
このコードの「現場のこだわり」
- 遅延初期化: `useEffect`内でDOMを生成することで、SSR(サーバーサイドレンダリング)環境での衝突を避けている。
- 自動クリーンアップ: 不要になったDOMノードを放置するのはメモリリークの元だ。最後にしっかり掃除をする。これができるかどうかが、プロと素人の分かれ道だ。
—
3. ブラウザはどう処理しているのか?
ここが重要だ。Portalを使っても、Reactのイベントバブリングは維持される。
これが`createPortal`の最大の凄みだ。通常のDOM操作で`document.body`に要素を直接移すと、Reactのコンテキストやイベント伝播が断ち切られてしまう。しかし、Portal経由でレンダリングすれば、物理的には`body`直下にいても、Reactのツリー構造上は元のコンポーネントの直下にあるものとして扱われる。
つまり、Portal内のボタンを押せば、その親コンポーネントで定義した`onClick`ハンドラが正しく発火する。この「コンポーネントの論理構造」と「DOMの物理配置」の乖離を隠蔽する技術こそが、Reactの完成度の高さを示しているんだ。
—
4. 運用上の注意点とベストプラクティス
最後に、シニアとしてのアドバイスをいくつか。
1. z-indexの管理: Portalを使っても`z-index`の戦いは終わらない。CSS変数を活用して、モーダルやツールチップのスタックコンテキストを管理する仕組みをグローバルに持たせるのが定石だ。
2. フォーカス管理: モーダルを開いた際、キーボード操作のフォーカスがモーダル内にトラップされるか確認してほしい。PortalはDOMの外側に移動するため、フォーカスの移動が直感的でなくなることがある。`react-focus-lock`のようなライブラリを併用するのが実務の近道だ。
3. 乱用厳禁: Portalは強力なハックだ。何でもかんでもPortalに放り込むと、DOM構造がカオスになり、デバッグが極めて困難になる。モーダル、ツールチップ、ドロップダウンなど、「画面の構成上、階層を飛び越える必要があるもの」だけに限定しよう。
—
どうだい? `createPortal`はただの「DOM移動ツール」ではない。ReactというフレームワークがDOMという制約とどう向き合っているかを象徴する、非常に賢い設計なんだ。
現場で困ったときは、いつでもこのコードを思い出してくれ。また何か壁にぶつかったら、いつでも聞きに来るといい。一緒にコードを磨いていこうぜ。

コメント