ちゃんなるぶろぐ

エンジニア5年生🧑‍💻 オライリーとにらめっこする毎日。

【5分でわかる!】Reactで複数のDOM要素にレンダリングする(レンダリング前にReact Hooksを呼ぶ)

どうも、ちゃんなるです!

今回は過去の2つの記事の内容を合わせてコーディングします🔥(ソースコード on Github)。

chan-naru.hatenablog.com

chan-naru.hatenablog.com

概要

今回やりたいこと

Reactコンポーネント内へレンダリング対象となる複数のDOM要素をPropsとして渡し、それ用いてレンダリングを行う。

おことわり

今回実装する内容は画面の表示はできますがWarningが出ます。

正攻法ではないrender()の使い方をしているためです。

ご了承くださいませ🙇‍♂️

実現方法

まず、レンダリングしたい要素を取得します。そしてそれをAppコンポーネントに渡します。

このスコープ内では画面上に影響を与えないよう仮のDOM要素(ここではtentativeElement)を作成し、それに対してAPPコンポーネントレンダリングします。

// index.js

import ReactDOM from 'react-dom/client'
import { App } from './App'

/*レンダリングしたい要素*/
const domContents = document.querySelectorAll('#selected')

/*Reactコンポーネントを実行させるための仮のDOM要素*/
const tentativeElement = document.createElement('div')
const tentativeRoot = ReactDOM.createRoot(tentativeElement)

tentativeRoot.render(
  /*Reactコンポーネントにレンダリングしたい要素を渡す
    (レンダリングはAppコンポーネント内部にて)*/
  <App domContents={domContents} />
)

ちなみにHTMLの内容は下記のようなものです。

<div>
  <div id="selected" >
  </div>
  <div>
    ...
  </div>
  <div id="selected" >
  </div>
</div>

Appコンポーネントの内容です。

// App.tsx

import ReactDOM from 'react-dom/client'

type DomContents = {
  domContents: NodeListOf<Element>
}

export const App = (props: DomContents) => {
  /*ここはReactコンポーネント内部なので、任意のReact Hooksが使える
    Hooksによって作成した情報などをレンダリング時に渡してあげられる*/

  /*Propsとしてレンダリングの起点となるDOM要素を受け取り、*/
  props.domContents.forEach(element => {
    /*このコンポーネント内でcreateRootに渡してあげる*/
    const root = ReactDOM.createRoot(element)
    root.render(
      <Content />
    )
  })
  return null
}

const Content = () => {
  return (
    <h1>Hello, World!!!</h1>
  )
}

実行結果の例(一部HTMLタグに背景色を付与しています)

ちなみに、下記ふたつのWarningが出ます。

  • Warning: Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate.render()にPropsやStateのような状態を持つ変数(ここではprops)を渡すのは危険です。レンダリング時に状態を変更するような処理があるとレンダリング処理が無限ループするかもしれません。

今回はpropsとして渡しているDOM要素に変更を加える処理がないので、無限ループの心配はありません。DOM要素をPropsで渡さずApp.tsx内で取得することで解消できるかもしれません。

  • Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it.createRoot()に渡された要素の内部で再度createRoot()を呼んでます。もしroot(ここではindex.js内でcreateRoot()して作られたtentativeRootを指す)を更新したいならば、代わりにroot.render()を呼び出しましょう。

今回はcreateRoot()に渡している要素はそれぞれ異なります。なのでこのWarningを解消するすべはなさそうです。

まとめと課題

Warningが出てはしまうものの、JSファイル(index.js)側ではなくReactコンポーネントApp.tsx)側で複数のDOM要素を起点としたレンダリング処理を実行させられました👍

上述した2つのエラーのうち1つ目は「Propsの受け渡しをしないようにする」、すなわち「Appコンポーネント内でDOM要素を取得するようにする」ことで解決するのでは?と思い次のように修正してみましたが…結果変わらず💦

// App.tsx

import ReactDOM from 'react-dom/client'

const domContents = document.querySelectorAll('#selected')

export const App = () => {
  domContents.forEach(element => {
    const root = ReactDOM.createRoot(element)
    root.render(
      <Content />
    )
  })
  return null
}

const Content = () => {
  return (
    <h1>Hello, World!!!</h1>
  )
}

Warningを回避する方法、そもそももっと良い複数DOMへのレンダリング方法がわかったら投稿します🙋‍♂️

環境

実装や動作確認の際に使ったツールたちです。

OS macOS Monterey Version 12.5
Node.js v18.6.0
create-react-app v5.0.1

*対象のアプリはcreate-react-appを用いて作成しました。

関連資料