いつuseCallbackを使うべきなのか
アソビュー! Advent Calendar 6日目です。
フロントエンドエンジニアの野口です。
今回はReactのHooks APIの一つであるuseCallbackの使い所について書いていきます。
useCallbackとは
メモ化したコールバック関数を返すHooks APIです。
useCallbackを使うことで再レンダー時の不要な関数の再生成を防ぐことが出来ます。
使い方
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b])
公式のサンプルをもとに説明します。
useCallbackは、以下の引数を取ります。
- 第1引数: メモ化するコールバック関数
- 第2引数: コールバック関数が依存する値(deps)
() => doSomething(a, b)
上記のサンプルだとこのコールバック関数はメモ化され、depsに指定した値が変わらなければ関数は再生成されません。
※depsの値はメモ化された関数を使うのか、関数を再生成するのかを判定する重要な値になります。設定のし忘れやミスは、不要な再生成やバグのもとになるのでESlintのexhaustive-deps を有効にすることが公式でも推奨されています。
useCallbackの使い所
まず、useCallbackを使うメリットは主に
- 関数の再生成コストを抑制
- React.memoやreact-reduxのconnectされたコンポーネントの再レンダーを抑制
ただし、1.の場合はuseCallbackを使うことでパフォーマンスが悪くなるケースも多いため、Profiler APIで計測しつつ使用するか、もしくは”1.の目的のためだけには使用しない”のが良いと思います。(関数の再生成のコストよりdepsの比較のコストなどが高くなる可能性があるため)
そのためuseCallbackの使い所は2.のケースになります。
React.memoに軽く触れながら説明していきます。
React.memo
React.memoはHOCであり、memoでラップしているコンポーネントのpropsに変更がなかった場合、最後に描画した結果を再利用します。
以下にサンプルを作成しています。consoleのlogを見ながらボタンを押してみてください。
- React.memo適用前
- React.memo適用後
適用前だと、状態に変更がないTitleコンポーネントも描画されてしまっていますが、適用後だと不要な描画を抑制できているのがわかります。
しかし、まだCounterButtonのコンポーネントは再描画の必要がないのに描画されてしまっています。
これは、propsとして渡しているonClickが影響しています。functional component内に定義したアロー関数はレンダーのたびに毎回再生成されてしまうため、propsに変更があったと判定され再レンダーされています。
ここで使えるのがuseCallbackです。
useCallbackで関数の再生成を防ぐことで、CounterButtonの不要な再レンダーを抑制できます。
- React.memo × useCallback適用後
注意点
useCallbackは、React.memoなど参照の同一性をみるように最適化されたコンポーネントのみ効果があります。
- React.memo
- react-reduxのconnect
- PureComponentを継承するclassコンポーネント
- shouldComponentUpdateを実装したclassコンポーネント
- (recomposeのpureでラップしたコンポーネント) など
そのためそれ以外のコンポーネントや直接HTML要素にコールバック関数を渡す以下のような場合では効果がありません。
const onClick = useCallback(() => doSomething(a), [a])
return <button onClick={onClick} />
どこまで最適化すべきか
useCallbackに関して言えば、React.memoなどを使ったコンポーネントにアロー関数を渡す場合は、基本的にuseCallbackを使うべきだと思います。 コンポーネントの再レンダーコストを抑えられるからです。
ただ、depsの計算コストはかかってくるので、React.memoを使うかどうかと合わせProfiler APIなどで検証しつつ最適化を進めていくのがベターになりそうです。
個人的には、こちらの記事にもある通り再レンダーによる最適化に時間をかけすぎるのは賢明ではないので、判断迷う場合にはuseCallbackを使わないというのも選択肢の一つとしてありだと思っています。
おわりに
useCallbackはうまく使えばパフォーマンスを改善することができる有用なhooksですが、意図せずパフォーマンスを悪化させてしまうケースもあります。
アソビューでもそうでしたが、どこまで最適化するかチームメンバーと話し合い対応していくのが良いと思います。
参考
https://qiita.com/uehaj/items/99f7cd014e2c0fa1fc4e
https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2
https://ja.reactjs.org/docs/hooks-reference.html
https://kentcdodds.com/blog/usememo-and-usecallback
https://dmitripavlutin.com/use-react-memo-wisely/