ゆとり日記

心にゆとりを持って生きたいプログラマーの雑記です。気が向いたら書きます。

React Portalでmodalのレンダリング位置を制御する

Reactでmodalを実装する際に便利なものがあったのでブログに纏めてみます。ちなみに、React 16時代から利用できる機能なので目新しい話題ではないことを冒頭で断っておきます。

参考 : React v16の機能紹介

React Portalについて

React Portal は簡単に言うと「子コンポーネントレンダリングするDOMノードを指定する」仕組みです。Reactでは大元となるDOMに対してコンポーネントの親子関係を構築していくのが基本ですが、DOMの階層に外にいるDOMノードにコンポーネントレンダリングしたいケースも起こりえます。その際に利用するのがReact Portalです。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="robots" content="noindex">
  </head>
  <body>
    <!-- 大元となるDOM -->
    <div id="react-root"></div>
    <!-- React Portalを使用したレンダリング先となるDOM -->
    <div id="portal-root"></div>
  </body>
</html>

言葉では伝わりづらいので、実際のHTMLも載せてみます。主要なコンポーネントレンダリングされるのはid「react-root」のdivですが、特定のコンポーネントをid「portal-root」のdivに対してのレンダリングが可能になるのです。

modal実装時の悩みどころ

要素に「重なり順」という関係性が生まれ、子要素が親要素の下に表示されるのはHTMLの基本です。ですが、modalは親を飛び越えていく必要があります。

modalの表示に支障が出ているHTMLのサンプルを実際に作ってみました。

https://codepen.io/rukiadia/pen/gOxBvNz

modalがサイドメニューの下に潜り込んでしまう様子
modalがサイドメニューの下に潜り込んでしまう様子

私が作成したサンプルでは、modalが画面左のサイドメニューの下に潜り込んでいます。modal要素は画面全体に広がる挙動を想定されるので、この見た目は違和感を感じざるをえません。要素の重なり順をz-indexで複雑に制御しているアプリケーションでは、このような状況が容易に起こりえます。

【余談】 z-indexについて理解を深めたい方は「z-index」「スタッキングコンテキスト」をキーワードに参考記事を探してみるのがオススメです。

React Portalを使ってみる

import React from 'react'
import { createPortal } from 'react-dom'

const modalRoot = document.createElement('div')
modalRoot.setAttribute('id', 'portal-root')
document.body.appendChild(modalRoot)

export const Modal: React.FC<Props> = ({ children, closeModal, isOpen }) => {
  return createPortal(
    isOpen ? (
      <Overlay onClick={closeModal}>
        <Container onClick={(event) => event.stopPropagation()}>
          {children}
        </Container>
      </Overlay>
    ) : null,
    modalRoot
  )
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="robots" content="noindex">
  </head>
  <body>
    <!-- 大元となるDOM -->
    <div id="react-root"></div>
    <!-- レンダリング先のDOMをModalコンポーネントが動的に生成 -->
    <!-- <div id="portal-root"></div> -->
  </body>
</html>

「親コンポーネントから渡されるchildrenをmodal内に表示」「modalのレンダリング先となるDOMを生成」の2つの役割を持つコンポーネントのコード例です。(必要最低限のコードにするため、CSSの記述などは意図的に省いてます)

レンダリング先のDOMをあらかじめ用意することも可能ですが、modalに関する事情をコンポーネント内に纏めたかったのでこうしています。また、テストコードが書きやすくなる利点もあるので、動的に生成する手法がオススメです。

参考 : Testing LibraryのModalテストコード例

まとめ

以上がReact Portalの仕様例の説明になります。

react-modal のようなライブラリを使用すれば必要のない苦労にはなりますが、自作したいケースもあるかもしれません。その場合はReact Portalを使用することで「画面の表示」と「コンポーネント構造」を切り離して実装を進めていくと、開発が楽になる筈です。