[React Hook] レンダリングのチューニング ~オブジェクトをContextとして利用する場合 & サンプルコード~
概要
この記事について
かんたんな概要と結論
外部ライブラリのisDeepEqual、useDeepCompareEffectを使用することで
良い感じにDeep Compareを実装できる。
React Hookによるブラウザアプリ画面の作成の際、
レンダリングされるComponentの数を絞る(チューニングする)ことで
実行されるコードのステップ数を減らし、パフォーマンスを改善することができる。
オブジェクトをContextとして各Componentにわたす処理のコードにおいて、
いくつかの観点ごとにチューニングする必要があったため、
ここにそれを記したい。
レンダリングのチューニング
やりたいこと
React HookにおいてオブジェクトをContextとして各Componentにわたす際に、
オブジェクトを更新するたびに、通常はContextを使用するすべてのComponentが再レンダリングされる。
オブジェクトが再度同じ値に設定される場合に、
不要なレンダリングを防ぐような仕組みが、Reactの標準APIにないかと探したが、
どうやら存在しないようだった。
また、もう一つの課題として、
利用先のComponentでContextのオブジェクトを取得し、
その値(参照ではなく、内容)に変更があるたびにuseEffectを実行させたいというものがある。
useEffectはimmutableな値のみを依存関係に含めることができるので、
今回のようなオブジェクトの変更を察知することが難しい。
上記の2点について解決したい。
改善観点
- レンダリングされるComponentの数をチューニング
- useEffectの実行タイミングの調整
初期状態(サンプルデモ)
初期表示、およびSetterBoxButtonを二回押下した際のログは次のようになる。
1render Container //*** 初期表示ここから
2render Box
3render EffectBox
4render SetterBox
5run useDeepCompareEffect
6{foo: {…}, bar: {…}} //*** ここまで
7render Container //*** 一回目ここから
8render Box
9render EffectBox
10render SetterBox //*** ここまで
11render Container //*** 二回目ここから
12render Box
13render EffectBox
14render SetterBox //*** ここまで
15
- ボタン押下ごとにContainer Componentおよびすべての子コンポーネントが再レンダリングされている。
- EffectBoxのuseEffectが一回目のボタン押下時に実行されない。
(依存関係に空の配列[]
を渡しているためにそうなる。本来ならボタン押下でmyContextが変更した際に実行してほしい)
チューニングの実行
レンダリング対象の絞り込み
i. React.memoを使用
Containerの子コンポーネントのそれぞれを、
React.memoを用いてメモ化する。
引数の値が変更しなければ、再レンダリングを引き起こさないようにする。
ログは次のようになる。
1render Container //*** 初期表示ここから
2render Box
3render EffectBox
4render SetterBox
5run useDeepCompareEffect
6{foo: {…}, bar: {…}} //*** ここまで
7render Container //*** 一回目ボタン押下 ここから
8render Box
9render EffectBox //*** ここまで
10render Container //*** 二回目ボタン押下 ここから
11render Box
12render EffectBox //*** ここまで
13
子コンポーネントのうち、
SetterBox
は再レンダされなくなった。
しかし、
Box
およびEffectBox
はメモ化しても再レンダされる。
ボタン押下時にmyContextが更新され、myContext.Providerで渡される値が変化するため、
useContext(myContext)
によってmyContextを使用しているComponentはすべて再レンダされる。
ここで渡される値はオブジェクトの参照値なので、たとえ内容が等しくても「別のオブジェクト」と判定され、
レンダリングが実行される。
次のiiで、その挙動を改善したい。
ii. useRefを使用してDeep Compareを実行
Container Componentのcxt
変数が更新されたあと、
useRefでフックされたcxtRef
変数と、
Deep Equal
(ネストされた各値のすべてが等しいことを保証)によって内容を比較する。
myContext.Providerのvalueに渡されるのはcxt
ではなくcxtRef.current
となる。
Deep Equal
の実装には、外部ライブラリのfast-deep-equal
を使用する。
ログは次のようになる。
1render Container //*** 初期表示ここから
2render Box
3render EffectBox
4render SetterBox
5run useDeepCompareEffect
6{foo: {…}, bar: {…}} //*** ここまで
7render Container //*** 一回目ボタン押下 ここから
8render Box
9render EffectBox //*** ここまで
10render Container //*** 二回目ボタン押下 ここからここまで
11
二回目のボタン押下では、
myContextの内容は変更しないため、
Deep Compare
の結果cxtRef.current
変数の参照は変化しない。
したがって、Box
およびEffectBox
の再レンダは引き起こされない。
useEffectの実行タイミングの調整
useEffectの依存関係においてmutableな値を許容するトリックだが、
検索した結果useDeepCompareEffect
という便利なフックが公開されていた。
これを用いることによって、
myContextが変更したタイミングのいずれにおいても
EffectBoxのサイドエフェクト処理が走るようにできる。
ログは次のようになる。
1render Container //*** 初期表示ここから
2render Box
3render EffectBox
4render SetterBox
5run useDeepCompareEffect
6{foo: {…}, bar: {…}} //*** ここまで
7render Container //*** 一回目ボタン押下 ここから
8render Box
9render EffectBox
10run useDeepCompareEffect
11{foo: {…}, bar: {…}} //*** ここまで
12render Container //*** 二回目ボタン押下 ここからここまで
13
一回目のボタン押下でmyContextの内容が変更されるため、
EffectBox
内のサイドエフェクト処理が実行され、ログに出力されるようになった。
関連記事
- ご意見送信フォームを作成する③ [SPA by React Router]
- ご意見送信フォームを作成する② [React]
- [JavaScript] Web Componentsで明暗モード切替ボタンを作成する
- ご意見送信フォームを作成する① [プレーンTypeScript]
- [Javascript] IE11でRadioボタングループのvalueを取得できない事象&対策