【React】 ポータルを使って外のDOMに要素を飛ばす
ポータルを用いることで、そのコンポーネント外にあるDOMのノードに子コンポーネントをレンダリングできます。例えばメッセージの表示やツールチップなど、子要素以外にレンダリングする必要がある場合によく用います。
使い方
ポータルを作るためには、ポータルを対象となるDOMと、底に入れるための命令が必要になります。
まずはポータルしたい内容を表示する箱を用意しましょう。これは普通のHTMLに書きます。
<body> <!-- こっちはReactのマウント対象 --> <div id="root"></div> <!-- ここに表示したい --> <div id="modal"></div> </body>
次に、ポータルするコンポーネントを作成します。作成にはReactDOM.createPortal
を用います。
import ReactDOM from 'react-dom'; export default function ModalComponent(props) { return ReactDOM.createPortal( props.children, document.getElementById("modal"), ) }
props.children
は、コンポーネントの呼び出し側で子要素に入れた要素でした。そのため、呼び出す側では次のように書きます。
import ModalComponent from './components/ModalComponent'; export default function App() { return ( <> <p>hogehoge</p> <ModalComponent> <p>modal text</p> </ModalComponent> </> ); }
ModalComponent
は上で作成したコンポーネントです。そこの子要素に表示したい内容を入れ、それをコンポーネントのprops.children
で受け取ります。
この書き方は下の記事で書いています。
このようにして書くと、次のように表示されます。
<body style="cursor: url(""), default;"> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"><p>hogehoge</p></div> <div id="modal"><p>modal text</p></div> </body>
Reactがマウントしたのは#root
ですが、ポータルを利用することで#modal
に表示を飛ばすことができました。
要素の制御
ポータルは、あくまで表示する場所を指定した場所に飛ばしているだけです。そのため、propsやifでの表示非表示など、普通のコンポーネントと同じように行うことができます。
export default function App() { // モーダル表示を管理するステート const [isViewModal, setIsViewModal] = useState(false); const toggleModal = useCallback(() => { setIsViewModal(!isViewModal); }, [isViewModal]); return ( <> <button onClick={toggleModal}>モーダル表示切り替え</button> <ModalComponent visible={isViewModal}> <p>modal text</p> </ModalComponent> </> );
export default function ModalComponent(props) { if (!props.visible) { return (<></>); } else { return ReactDOM.createPortal( props.children, document.getElementById("modal"), ) } }
イベントのバブリング
ポータルに入れたDOMでイベントが発生した場合 (クリックイベントなど)、通常と同じようにバブリングが発生します。
バブリングとは
まずはバブリングの復習です。バブリングとは、子要素で発生したイベントが親要素に伝わっていくことを言います。
つまり、親要素でイベントを設定しておけば、そのどの子要素でイベントが発生しても反応できるということです。

次の例では、子要素でクリックしても親要素のイベントを発火できる例です。
See the Pen bubbling sample by Totori (@souki202) on CodePen.
JSXの親子関係でバブリング
Reactでポータルを利用した要素で発生したイベントは、Reactで書いた通りの親子関係でバブリングが発生します。
例えば、次のような例の場合を見てみます。
export default function App() { const [isViewModal, setIsViewModal] = useState(false); const toggleModal = useCallback(() => { setIsViewModal(!isViewModal); }, [isViewModal]); return ( <> <button onClick={toggleModal}>モーダル表示切り替え</button> <div onClick={toggleModal}> <ModalComponent visible={isViewModal}> <p style={{backgroundColor: "#fcc"}}>modal text</p> </ModalComponent> </div> </> ); }
この場合、例えば次のようなHTMLが出力されます。
<div id="root"><button>モーダル表示切り替え</button><div></div></div> <div id="modal"><p style="background-color: rgb(255, 204, 204);">modal text</p></div>
ポータルで飛ばしたModalComponent
の中身が#modal
側に表示されていることがわかります。この状態でmodal text
の部分をクリックした場合はどうなるのでしょうか。
その場合は、JSXで書いたイベントであれば、実際のDOMではなく、JSXで書いた親子関係が使用されます。つまり、toggleModal
が実行されます。
まとめ
ポータルを使用することで、実際の親子関係から外して、全く別の場所に要素を表示できます。この時、イベントはJSXで書いた親子関係が使用されるため、直感的にイベントを書くことができます。
モーダルやツールチップなど、親子関係から切り離して表示したい内容に活用していきましょう。
