createPortal

createPortal を使うことで、DOM 上の別の場所に子要素をレンダーすることができます。

<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>

リファレンス

createPortal(children, domNode, key?)

ポータルを作成するには、createPortal を呼び出し、JSX とそれをレンダーする先の DOM ノードを渡します。

import { createPortal } from 'react-dom';

// ...

<div>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>

さらに例を見る

ポータルは DOM ノードの物理的な配置だけを変更します。それ以外のすべての点で、ポータルにレンダーする JSX は、レンダー元の React コンポーネントの子ノードとして機能します。例えば、子は親ツリーが提供するコンテクストにアクセスでき、イベントは React ツリーに従って子から親へとバブルアップします。

引数

  • children: React でレンダーできるあらゆるもの、例えば JSX(<div /><SomeComponent /> など)、フラグメント (<>...</>)、文字列や数値、またはこれらの配列。

  • domNode: document.getElementById() によって返されるような DOM ノード。ノードはすでに存在している必要があります。更新中に異なる DOM ノードを渡すと、ポータルの内容が再作成されます。

  • 省略可能 key: ポータルの key として使用される一意の文字列または数値。

返り値

createPortal は、JSX に含めたり React コンポーネントから返したりすることができる React ノードを返します。React がレンダー出力内でそのようなものを検出すると、渡された children を渡された domNode の内部に配置します。

注意点

  • ポータルからのイベントは、DOM ツリーではなく React ツリーに沿って伝播します。例えば、ポータル内部でクリックが起き、ポータルが <div onClick> でラップされている場合、その onClick ハンドラが発火します。これが問題を引き起こす場合、ポータル内部からイベント伝播を停止するか、ポータル自体を React ツリー内で上に移動します。

使用法

DOM 内の別部分へのレンダー

ポータルにより、コンポーネントが子の一部を DOM 内の別の場所にレンダーできるようになります。これにより、コンポーネントの出力の一部を、自身の含まれているコンテナの外に「脱出」させることが可能です。例えばコンポーネントから、ページの他の要素の上部や外部に表示されるモーダルダイアログやツールチップを表示することができます。

ポータルを作成するには、何らかの JSX行き先の DOM ノード を渡した createPortal の呼び出し結果をレンダーします。

import { createPortal } from 'react-dom';

function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
);
}

React は、渡した JSX に対応する DOM ノードを 渡した DOM ノード の内部に配置します。

ポータルがなければ 2 つ目の <p> は親の <div> の内部に配置されますが、ポータルはそれを document.body に「テレポート」させます。

import { createPortal } from 'react-dom';

export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      )}
    </div>
  );
}

2 つ目の段落が親の <div> の境界線の外側に表示されていることに注意してください。開発者ツールで DOM 構造を調べると、2 つ目の <p> が直接 <body> に配置されていることがわかります。

<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>This child is placed inside the parent div.</p>
</div>
...
</div>
<p>This child is placed in the document body.</p>
</body>

ポータルは DOM ノードの物理的な配置だけを変更します。それ以外のすべての点で、ポータルにレンダーする JSX は、レンダー元の React コンポーネントの子ノードとして機能します。例えば、子は親ツリーが提供するコンテクストにアクセスでき、イベントは React ツリーに従って子から親へとバブルアップします。


ポータルを使ったモーダルダイアログのレンダー

ポータルを使用して、ページ内の他の要素上に浮かんで表示されるモーダルダイアログを作成することができます。ダイアログを呼び出すコンポーネントが overflow: hidden やダイアログに干渉する他のスタイルを持つコンテナ内にあっても問題ありません。

この例では、2 つのコンテナはモーダルダイアログの表示を妨げるようなスタイルを持っていますが、ポータルを使ってレンダーされた方は影響を受けていません。DOM 内ではモーダルは親 JSX 要素の中に含まれていないからです。

import NoPortalExample from './NoPortalExample';
import PortalExample from './PortalExample';

export default function App() {
  return (
    <>
      <div className="clipping-container">
        <NoPortalExample  />
      </div>
      <div className="clipping-container">
        <PortalExample />
      </div>
    </>
  );
}

落とし穴

ポータルを使用する際には、アプリを正しくアクセシブルにすることが重要です。例えば、ユーザが自然にポータルの内または外へフォーカスを移動できるよう、キーボードフォーカスを管理する必要があるかもしれません。

モーダルを作成する際には、WAI-ARIA のモーダル作成実践ガイドに従ってください。コミュニティパッケージを使用する場合は、それがアクセシブルであり、このガイドラインに従っていることを確認してください。


非 React のサーバマークアップに React コンポーネントをレンダー

React で構築されていない静的ページあるいはサーバレンダーされたページの一部に React ルートがある場合にも、ポータルは有用です。例えば、ページが Rails のようなサーバフレームワークで構築されている場合、サイドバーなどの静的な部位の内部に操作可能なエリアを作成することができます。別々の React ルートを複数用いる場合と異なり、ポータルを使うとアプリを単一の React ツリーとして扱うことができ、部位ごとに DOM 内の異なる場所にレンダーさせつつも state を共有可能です。

import { createPortal } from 'react-dom';

const sidebarContentEl = document.getElementById('sidebar-content');

export default function App() {
  return (
    <>
      <MainContent />
      {createPortal(
        <SidebarContent />,
        sidebarContentEl
      )}
    </>
  );
}

function MainContent() {
  return <p>This part is rendered by React</p>;
}

function SidebarContent() {
  return <p>This part is also rendered by React!</p>;
}


非 React DOM ノードに React コンポーネントをレンダー

React 外で管理されている DOM ノードの内容を管理するためにポータルを使用することもできます。例えば、非 React のマップウィジェットと統合していて、ポップアップ内に React のコンテンツをレンダーしたいとします。これを行うには、レンダーする DOM ノードを格納するための popupContainer state 変数を宣言します。

const [popupContainer, setPopupContainer] = useState(null);

サードパーティのウィジェットを作成する際に、ウィジェットが返す DOM ノードを格納しておき、その内部にレンダーが行えるようにします。

useEffect(() => {
if (mapRef.current === null) {
const map = createMapWidget(containerRef.current);
mapRef.current = map;
const popupDiv = addPopupToMapWidget(map);
setPopupContainer(popupDiv);
}
}, []);

このようにしておけば、popupContainer が利用可能になったところでその中に createPortal を使って React コンテンツをレンダーすることができるようになります。

return (
<div style={{ width: 250, height: 250 }} ref={containerRef}>
{popupContainer !== null && createPortal(
<p>Hello from React!</p>,
popupContainer
)}
</div>
);

以下に、試してみることができる完全な例を示します。

import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createMapWidget, addPopupToMapWidget } from './map-widget.js';

export default function Map() {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const [popupContainer, setPopupContainer] = useState(null);

  useEffect(() => {
    if (mapRef.current === null) {
      const map = createMapWidget(containerRef.current);
      mapRef.current = map;
      const popupDiv = addPopupToMapWidget(map);
      setPopupContainer(popupDiv);
    }
  }, []);

  return (
    <div style={{ width: 250, height: 250 }} ref={containerRef}>
      {popupContainer !== null && createPortal(
        <p>Hello from React!</p>,
        popupContainer
      )}
    </div>
  );
}