はじめに
Reactで開発をしていると、ほぼ必ず目にするのがuseCallback。
しかし「なんとなく付けている」「つけたりつけなかったりで理由が曖昧」という人も多いはずです。
本記事では、useCallbackの基礎から実戦で迷いがちなポイントまでを、できるだけシンプルに、かつ実務レベルで理解できるようまとめました。
useCallbackとは?
useCallbackの定義
useCallbackは、再レンダー間で関数定義をキャッシュできるようにするReactフックです。
参照:React公式
簡単に言うと、
「同じロジックの関数を、毎回作り直さずに再利用するための仕組み」。
なぜ関数を「毎回作り直す」のか?
Reactのコンポーネントはレンダリング(=コンポーネント関数の実行)されるたびに、再評価される。
そのため、下記のように関数を定義すると、
レンダリングのたびに「新しい関数オブジェクト」として生成される。
const handleClick = () => {
console.log('clicked');
};
- 毎回新しい関数が生成される(レンダリングのたび)
- 子コンポーネントに渡すと、毎回「propsが変わった」扱いになる
- その結果、不必要な再レンダリング(Reactがコンポーネントを再実行)が起きる
上記を防ぐためのuseCallback。
前提として理解必要なところ
-
レンダリング=コンポーネントを実行して仮想DOMを作り直すこと(React)
ブラウザ描画(ペイント)とは別物。 -
レンダリング発生タイング
1. 初回レンダリング
2.stateが更新された時
3. 親コンポーネントがレンダリングされた時 -
レンダリングされたからといって必ず画面が変わるわけではない
差分がなければDOM更新もペイントも起きない。 -
入力や
state更新があると必ずレンダリングが起きる
そのため、入力内容のリアルタイム反映などは自然に実現できる。 -
memoしていない限り、propsが変わらなくても子は毎回レンダリングされる
→ Reactの基本仕様として、親の再レンダリング=子も再実行。
useCallbackの基本形
コード
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
ポイント
-
[]が依存配列 - 中の値が変わらない限り、関数は同じ参照のまま保持される(メモ化)
依存配列とは
概要
依存配列に何かが入っていると、
その値が変わった時だけ、関数を作り直す
const handleChange = useCallback(() => {
setCount(count + 1);
}, [count]);
count が変わるたびに新しく生成される。
逆に count が変わらなければ、同じ関数のまま再利用される。
疑問
①setStateは依存配列に入れる?
結論
setState関数(setXxx)は依存配列に入れなくてOK
説明
Reactが保証した安定した参照(毎レンダリングで変わらない)。
そのため、useCallbackの依存配列に入れる必要はない。
参考:stack overflow
②propsの関数は依存配列に入れる?
結論
propsで受け取った関数(例:onClose)は、依存配列に入れるべき。
例
const handleRowClick = useCallback(
(params) => {
onClose();
console.log(params.row);
},
[onClose], // ←これが正解
);
ポイント
- 親の
onCloseが変わると、子のhandleRowClickも更新される必要がある - 依存配列に入れないと、古い関数を参照し続けてバグの原因になる
※例はpropsの関数であるが、propsの値であっても依存関係に入れる
例外
依存配列に入れなくても良いprops
- 親が
useCallbackで安定化済みの関数 -
propsが変わらないことが保証されているケース- 定数(ほぼリテラル)
- 親でメモ化(memo / useMemo)されている値
③イベントハンドラ全部useCallbackにすべき?
結論
NO!
解説
useCallbackを使うべきケース
useCallbackの役割はシンプルで、「関数の参照(=メモリアドレス)を固定する」こと。
関数の中身が同じでも、コンポーネントが再レンダリングすると毎回「別の関数オブジェクト」として作り直される。
それが原因で子やライブラリ側が「props変わった!」と勘違いして無駄に再レンダリングする時に効果がある。
概要
useCallbackを使うと、依存配列が変わらない限り、同じ参照の関数を使い回せる。
ただし、「関数の参照が安定する」こと自体が、必ずしも再レンダリングの抑制につながるわけではない。
Reactでは通常、親が再レンダリングされると子も一緒に再レンダリングされるためからである。
useCallbackが意味を持つのは、「関数の参照が変わったかどうかを見て挙動を変える子コンポーネント」の場合になる。
例えば、子がReact.memoでメモ化されている場合、Reactはpropsの参照が前回と同じかどうかで再レンダリングするかを判断する。
その場合、useCallbackをしていないものだと、親で関数を毎回作り直すことになり、関数の中身が同じでも「参照が変わった」と判断され、再レンダリングが発生する。
上記の場合、useCallbackで関数の参照を安定させることで、子は「変わっていない」と判断でき、無駄な再レンダリングを防げる。
同様に、子コンポーネント内のuseEffectやuseMemoの依存配列にその関数が含まれている場合も、参照の変化が重要になる。
親で関数を作り直すたびに参照が変わると、effectの再実行や再計算が発生しますが、useCallbackを使えばそれを防げる。
子がReact.memoされていない場合や、子のuseEffect・useMemoの依存配列にその関数を使っていない場合は、子は親の再レンダリングに合わせて毎回レンダリングされるため、useCallbackを使ってもパフォーマンスへの影響はほとんどない。
2.メモ化されたコンポーネント(React.memo)に渡す関数
React.memoはpropsを浅い比較(===)でチェックする。
そのため、親が毎回新しい関数を作る構造だと、memoの判定は常に「変更あり」となり、最適化がまったく効かなくなる。
const Child = memo(({ onClose }) => {
/* ... */
});
const Parent = () => {
const onClose = useCallback(() => {/* ... */}, []);
return <Child onClose={onClose} />;
};
ここでuseCallbackを使うと、参照が毎回変わらないためmemoが正しく働き、子の不要な再レンダリングを防げる。
逆に、子がmemoされていないなら、親が再レンダリングされた時点で必ず子も再レンダリングされるため、useCallbackの有無は最適化に影響しない。
3.useEffect / useMemoの依存に入れる関数
関数を依存配列に含める場合、参照が変わるとeffectやmemoが再実行される。
上記が意図せず何度も実行されるのを防ぐために、useCallbackで参照を安定させる。
4.高頻度レンダリングされるコンポーネントに渡す関数
DataGrid・Table・Listなどは、スクロール・ホバー・ソート・ページ変更などで、描画が頻繁に走る。
そのとき、子に渡す関数が毎回新しい参照だと、内部の最適化(仮想化やメモ化)が効きづらくなる。
大量の行・セルに同じ関数を渡す場合ほど、参照を安定させる効果が出やすい。
そのため、renderCell・onRowClickなど「全行に渡るコールバック」はuseCallbackの候補になる。
使用するかの判断基準
- その関数、外に渡してる?
渡してない→基本いらない - 渡してるなら、相手は参照変化に敏感?
- React.memoされた子
- useEffect依存に入ってる子/自分
- DataGrid等、参照でキャッシュするライブラリ→使う価値があり
- それでも迷うなら一旦なしで書いて、実測で重かったら足す
(React DevTools Profilerで、どこが無駄に再レンダリングしてるかを確認できるらしいので、試す)
まとめ
思った以上に長くなってしまいました。
useMemoとの違いも盛り込みたいと思っていましたので、さらに別記事で追加しようと思います。