React.memo を濫用していませんか? 更新頻度で見直す Provider 設計
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