📝

React.memo を濫用していませんか? 更新頻度で見直す Provider 設計

2020/11/02に公開

React.memoを適用すれば、コンポーネントの不要な ReRender を防ぐことができます。しかしながら、Provider 設計・バケツリレーの見直しを行うことで、React.memoを使わずとも、ReRender の抑止は可能です。

最適な Context Provider 設計とすることで、React.memo使用によるオーバーヘッド削減が期待できます。そして「過剰な Provider 分割も場合によっては不要」ということを解説していきます。

3つのパターン

サンプルリポジトリを用意しました。(Next.jsで作っていますが、特に意味はありません)「+1」押下でカウントアップし「input text」入力で、入力内容が更新される簡単なサンプルです。

サンプル画像

こちらでは、以下3つのパターンが用意されています。ReRender されている様子は console.log 出力のほか、React Developer Tools でも確認できます。いずれのパターンにもReact.memoは用いておらず、表示上で得られる出力結果は同一です。それでいて、ReRender 頻度は異なるものとなっています。

  • モノリス Provider パターン
  • モノリス Parent パターン
  • マイクロ Hook パターン

モノリス Provider パターン

こちらは「object」を Provider value としているケースです。「Provider を幾重にも重ねるのが億劫だから…」という、運用で起きがちな、モノリスなProviderです。object の内訳は次のとおりです。

type CTX = {
  count: number;
  text: string;
  increment: () => void;
  handleChangeText: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

Context value を object にしてしまっているため、反応して欲しくない Consumer コンポーネントでも反応してしまっている点が問題です。今回の様な 「count・text いずれも更新頻度が高い」 サンプルの場合、明確なアンチパターンです。

  • PROS: 値の更新が頻繁でなければ問題になることは少ない
  • CONS: 値の更新が頻繁なものが含まれた途端、rerender が多発する

このパターンの場合、子孫コンポーネントのReact.memo化は不可避となります。

モノリス Parent パターン

こちらは「値」を Provider value とし、細分化しているケースです。しかしながら、Function Component(子コンポーネント)に値を注入するため、親コンポーネントで バケツリレーが発生してしまっています。

  • PROS: 末端コンポーネントが Context 非依存 (再利用性:高)
  • CONS: React.memo化 が末端コンポーネントで必要になる
export const CounterContext = React.createContext<number>(0);
export const TextContext = React.createContext<string>("");
export const Parent: React.FC = () => {
  console.log("Parent");
  const count = React.useContext(CounterContext);
  const text = React.useContext(TextContext);
  return <Child count={count} text={text} />;
};

これにより、それぞれの子コンポーネントでReact.memo化する必要が出てきます。Context を細分化した利点が活かせていません。依存性の低い AtomicDesign パーツ集約から、このパターンは生まれやすいです。

マイクロ Hook パターン

今回の様な 「count・text いずれも更新頻度が高い」 サンプルの場合、最適解となります。モノリス Parent パターンと同様、Provider を分割し、末端コンポーネントでそれぞれ興味のある Context value を取得します。

  • PROS: React.memo化 が末端コンポーネントで不要になる
  • CONS: 末端コンポーネントが Context 依存している (再利用性:低)
export const ChildCount = () => {
  console.log("ChildCount");
  const count = React.useContext(CounterContext);
  return <>{count}</>;
};
export const ChildText = () => {
  console.log("ChildText");
  const text = React.useContext(TextContext);
  return <>{text}</>;
};

CONS で示している様に、この末端コンポーネントは Context なしには成立しません。しかしながら、さらなる子コンポーネントで値を props で受け取る様にしていれば、その子コンポーネントは Context 依存がなくなります。

まとめ

今回の様な 「count・text いずれも更新頻度が高い」 サンプルの場合「マイクロ Hook パターン」が最適解となりますが 「包含する全ての値において更新頻度が低い」場合、モノリス Provider パターンが最適解 となることもあります。Next.js の状態管理 2020 で紹介している様な、getStaticProps / getServerSidePropsで初期値を Context に渡す場合です。更新が見込まれない値を分配する場合、Provider 細分化が、かえってオーバーヘッドになり得ます。

進め方として、はじめは「モノリス Provider」としておきつつ、更新頻度が高い値が含まれていることを確認した段階で「マイクロ Provider」として「当該値だけを保持した Provider」を切り出していく方法が良いと思います。

Provider を用いた状態管理は React 標準APIということもあり、よく用いられていることでしょう。少し Provider 設計を見直すことで、React.memo化をする必要はなくなります。React.memoは本当に使わざるをえない場合に留めていきましょう。

Discussion