🔥

任意のテンプレートエンジン(HTML) で React を部分的に用いる際に 便利な createPortal API について

2021/09/06に公開

React は基本的には UI を記述するライブラリですので、サーバーサイドは別の言語を用いることが多いかと思います。その場合は何らかのテンプレートエンジンで html を生成してその data 属性を返してサーバーの変数を React の Props を渡したいという場合がしばしばあるのではないかと思われます。

(Next とか React とシームレスにつなげる Web フレームワーク以外は全部そういうときありそう。)

そんなときに便利な createPortal を使った小技を紹介します。

テンプレートエンジンはRubyにおける Haml、Erb , JavaScriptにおける Pug, Ejs ,PHP であれば Smarty などのイメージです。

そうした場合、すべてのコンポーネントで変数や状態をシェアしたい場合どのようにしていますか?

React は ReactDOM.render でコンポーネントをレンダリングしたい対象箇所に HTMLelement を指定してごとにレンダリングするのが普通です。、例えば user_id をそれぞれの React Components の Props に渡したいとします。

<body>
...
    <div
      id="app1"
      data-user-id="userID変数"
      >
    </div>
...
    <div
      id="app2"
      data-user-id="userID変数"
      >
    </div>
...
    <div
      id="app3"
      data-user-id="userID変数"
      >
    </div>
</body>

... import は略

// element から data 属性所得する関数
const getProps = (element: HTMLElement): ComponentProps<typeof App1> => ...

ReactDOM.render(
 <App1 {...getProps(document.getElementById('app1'))} />,
  document.getElementById('app1')
)

ReactDOM.render(
  <App2 {...getProps(document.getElementById('app1'))}/>,
  document.getElementById('app2')
)
 
 ReactDOM.render(
  <App3 {...getProps(document.getElementById('app1'))}/>,
  document.getElementById('app3')
)

どう見ても冗長ですが、ReactDOM.render では子要素にレンダリングすることしかできないし、仕方ありません...

しかし実は以下の便利メソッドが ReactDOM にはあります。

https://ja.reactjs.org/docs/portals.html

"ReactDOM.createPortal(child, container) ポータル (portal) は、親コンポーネントの DOM 階層外にある DOM ノードに対して子コンポーネントをレンダーするため"

ユースケースとしては子要素の階層外にあるモーダルなどのレンダリングに用いるということですが、今回のように自由自在な場所にレンダリングする箇所を指定したい & 共通の変数を持ちたいというときに便利です。

例えば前述の例で言えば以下のように書き換えられます。

<body>
...
    <div
            data-portal-id="app1"
      data-app1="変数"
      >
    </div>
...
        // class名でもいいが CSS などと衝突しないよう portal-idというのを付与すると便利
    <div
            data-portal-id="app1"
      data-app1="変数2"
      >
    </div>
...
     <div
            data-portal-id="app2"
      data-app1="変数2"
      >
    </div>
...
     <div
            data-portal-id="app3"
      data-app1="変数2"
      >
    </div>
...
    <div
            id="app-root"
      data-user-id="ユーザーID変数"
      >
    </div>
</body>

... import は略

type PortalID = 'app1' | 'app2' | 'app3'

const getNodeListByPortalID = (portalID: PortalID): HTMLElement[] => {
  return [].slice.call(
    document.querySelectorAll<HTMLElement>(`[data-portal-id="${portalID}"]`)
  )
}

const rootElement = document.getElementById('app-root')
const userID = rootElement?.dataset.userId

export const OutsideVariableContext = createContext<Context>({})

if (rootElement) {
  ReactDOM.render(
    <OutsideVariableContext.Provider userId={userID} >
       {getNodeListByPortalID('app1').map((element, index) => {
        return ReactDOM.createPortal(
          <App1
            key={'app1' + index}
            {...getPropsApp1(element)}
          />,
          element
        )
      })}
      {getNodeListByPortalID('app2').map((element, index) => {
        return ReactDOM.createPortal(
          <App2
            key={'app2' + index}
            {...getPropsApp2(element)}
          />,
          element
        )
      })}
      {getNodeListByPortalID('app3').map((element, index) => {
        return ReactDOM.createPortal(
          <App3
            key={'app3' + index}
            {...getPropsApp3(element)}
          />,
          element
        )
      })}
    </OutsideVariableContext.Provider>,
    rootElement
  )
}

// Context API で変数を所得する
const App1: FC<Props> => {
   const { userID } = useContext(OutsideVariableContext)
   ...
   <>
      ...
   </>
}

重複なく、冗長性が消えました。html データを渡し忘れるなども減るはずです。何が共通で渡すべき変数なのかわかりやすいといいですね。

特定のテンプレートエンジンで React アプリを書いた人なら伝わる内容かと思います... 書き方が下手でしたらすみません💦

Discussion