【React】childrenを用いたパフォーマンス最適化

3 min read読了の目安(約3300字

React.memoを使用する他に、childrenで要素を受け取ることによって子コンポーネントの再レンダリングを抑制することができます。この手法はContextを用いる際によく出てくるため、意識せずとも使用しているケースは多いのではないでしょうか?
例えば、以下のようなThemeProviderコンポーネントの状態が更新されたとしてもuseContextで値を受け取っているコンポーネント以外は再レンダリングされません。これはchildrenで要素を受け取って出力しているためです。

ThemeProvider.tsx
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState("light")
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme])

  return (
    <ThemeContext.Provider value={themeValue}>
      {children}
    </ThemeContext.Provider>
  )
}
App.tsx
const App = () => {
  return (
    <ThemeProvider>
      <SomeComponent />
    </ThemeProvider>
  )
}

再レンダリング抑制にはいくつかの手法がありますが、本記事ではどういった時にchildrenでパフォーマンス最適化したほうが良いのかについて考えていきたいと思います。

再レンダリングの抑制

以下のようなコンポーネントで想定します。

CountPage.tsx
const CountPage: React.VFC = () => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>カウントアップ</button>
      <p>{count}</p>
      <Body />
      <CounterFooter count={count} />
    </div>
  )
}

親コンポーネントが再レンダリングされると全ての子コンポーネントが再レンダリングされるため、countを更新すると状態を受け取っていない<Body />も再レンダリングされます。
countを複数のコンポーネントが依存しているため、状態管理を別コンポーネントに切り分けることもできません。

このような時に<Body />の再レンダリングを抑制するいくつかの方法を見ていきます。

1. React.memoでメモ化する

よくあるパフォーマンス対策の一つとして、BodyコンポーネントをReact.memoでメモ化します。
第2引数を使用しなければバグを生み出すことは基本的には無いため、必要最小限の労力で再レンダリングを抑制できます。
このコンポーネント内の値に<Body />が依存している場合、他の選択肢が限られるためほぼこの手法を使うことになると思います。

2. childrenで子要素として受け取る

そもそも今回の場合は<Body />はこのコンポーネント内の値に依存していません。ならば、このコンポーネント内で<Body>を呼び出す必要がなくchildrenで子要素として受け取る設計にすることもできます。

App.tsx
const CountPage: React.VFC = () => {
  return (
    <CountPageLayout>
      <CountBody />
    </CountPageLayout>
  )
}
CountPageLayout.tsx
type Props = {
  children: React.ReactElement;
}

const CountPageLayout: React.VFC<Props> = ({children}) => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>カウントアップ</button>
      <p>{count}</p>
      {children}
      <CounterFooter count={count} />
    </div>
  )
}

こうすることでコンポーネントとして状態の依存が明確になる上に、コンポーネントではなく要素を受け取ることによって<Body />は再レンダリングされず、パフォーマンス対策にもなります。
ちなみにchildrenは公式のコンポジション vs 継承 – Reactで解説されている通り、独自に定義したPropsで要素を渡しても問題ありません。

3. Context or 状態管理ライブラリを使用する

そもそも複数のコンポーネントでcountを利用するのだから共有しやすいように状態管理すべきだ。という場合はContextReduxRecoil等を使用し、親コンポーネントで状態を管理しないようにすれば<Body />の再レンダリングを抑えることができます。
countの状態に依存するコンポーネントを切り出し、対象のコンポーネント内でそれぞれ状態を参照するように修正します。

CountPage.tsx
const CountPage: React.VFC = () => {
  return (
    <div>
      <Count />
      <Body />
      <Footer />
    </div>
  )
}

ただしこの方法はプロジェクトにおける状態管理のルールに左右されやすく、コード量が増えて複雑になりがちなデメリットはあります。今回の場合はcountの状態管理の要件が膨れてきた場合に移行を検討するぐらいが良いかなと思います。

まとめ

パフォーマンス対策はコンポーネント設計・状態管理設計に大きく影響を受けるため、常にこれが正しいといった手法を決めるのは難しいように思えますが、頻出するパターン自体はあります。
パフォーマンス対策を考える際にchildren等で要素を受け取る事によって、再レンダリングを抑えることができないかを考えることも選択肢の一つとして持っても良いのでは無いかと思います。

参考記事

コンポジション vs 継承 – React
コンテクスト – React