【React】 ポータルを使って外のDOMに要素を飛ばす

ポータルを用いることで、そのコンポーネント外にあるDOMのノードに子コンポーネントをレンダリングできます。例えばメッセージの表示やツールチップなど、子要素以外にレンダリングする必要がある場合によく用います。

使い方

ポータルを作るためには、ポータルを対象となるDOMと、底に入れるための命令が必要になります。

まずはポータルしたい内容を表示する箱を用意しましょう。これは普通のHTMLに書きます。

<body>
    <!-- こっちはReactのマウント対象 -->
    <div id="root"></div>

    <!-- ここに表示したい -->
    <div id="modal"></div>
  </body>

次に、ポータルするコンポーネントを作成します。作成にはReactDOM.createPortalを用います。

ModalComponent.jsx
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で書いた親子関係が使用されるため、直感的にイベントを書くことができます。

モーダルやツールチップなど、親子関係から切り離して表示したい内容に活用していきましょう。

react portal thumb

役に立ったらシェアしよう!