今回、useCallbackを理解する上で、参考にさせていただいた記事がこちらです。
Your Guide to React.useCallback()
こちらを翻訳してまとめたものになります。掲載許可済みです。
Dmitri Pavlutinさん、ご協力ありがとうございます😢
「Good luck in your journey to mastering Frontend development!」
と、とても優しい方で、すっかりファンになってしまった。
その前に、関数の等価性チェックを理解する。
function factory() {
return (a, b) => a + b;
}
const sum1 = factory();
const sum2 = factory();
sum1(1, 2); // => 3
sum2(1, 2); // => 3
sum1 === sum2; // => false
sum1 === sum1; // => true
例えばfactory()から生成されたsum1
とsum2
は異なる関数オブジェクトであることがわかる。
sum1 === sum2 // => false
sum1 === sum1 // => true
全てのオブジェクトは、それ自身としか等しくない。
useCallbackの目的
const MyComponent = () => {
// handleClick is re-created on each render
const handleClick = () => {
console.log('Clicked!');
};
// ...
}
このhandleClick関数は、コンポーネントが再レンダリングされるたびに再生成されます。
そのため、レンダリングごとに異なるオブジェクトになります。
インライン機能は安価な(軽い?)なので、レンダリングごとに機能を作り直すことは問題になりません。
コンポーネントごとに数個のインライン関数があれば問題ありません。
※インライン関数とは、名前のついた無名関数のこと。たとえば以下のような関数のこと。
const handleClick = () => {
console.log('Clicked!');
};
しかし、場合によってはレンダリング間で1つの関数インスタンスを維持しておく必要があります。
- React.memo()でラップされた機能コンポーネントが、関数オブジェクトpropを受けとっている場合。
- useEffect(..., [callback])のように、関数オブジェクトが他のフックに依存している場合。
- 関数が何らかの内部状態を持っているとき、例えば関数がデバウンスやスロットルされているとき。
useCallback(callbackFun, deps)が役に立つのは以上3つのとき。
同じ依存関係の値(deps)が与えられると、hookはレンダリングの間に関数インスタンスを返す。
import { useCallback } from 'react';
const MyComponent = () => {
// handleClick is the same function object
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
// ...
}
handleClickは、MyComponentがレンダリングされる間、常に同じコールバック関数オブジェクトを保持するようになります。
良い使い方
例えば、とても大量のitemリストをレンダリングするコンポーネントがあったとします。
import useSearch from './fetch-items';
const MyBigList = ({ term, onItemClick }) => {
const items = useSearch(term);
const map = item => <div onClick={onItemClick}>{item}</div>;
return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);
リストはとても大きく、数百のアイテムがあるかもしれません。無駄なリストの再レンダリングを防ぐために、React.memo()
でラップしています。
import { useCallback } from 'react';
export default const MyParent = ({ term }) => {
const onItemClick = useCallback(event => {
console.log('You clicked ', event.currentTarget);
}, [term]);
return (
<MyBigList
term={term}
onItemClick={onItemClick}
/>
);
}
onItemClick
をuseCallback
にてラップしています。
useCallback(callbackFun, term)
のterm
が同じであれば、useCallback
は同じ関数オブジェクトを返します。
このような使い方をすれば、MyParentコンポーネントが再レンダリングされても、onItemClick関数オブジェクトは同じままで、MyBigListのメモ化が壊れることはありません。
悪い使い方
import { useCallback } from 'react';
const MyComponent = () => {
// Contrived use of `useCallback()`
const handleClick = useCallback(() => {
// handle the click event
}, []);
return <MyChild onClick={handleClick} />;
}
const MyChild = ({ onClick }) => {
return <button onClick={onClick}>I am a child</button>;
}
このコンポーネントでは、useCallback
を使う意味があるのでしょうか?
MyChild
コンポーネントは軽く、その再レンダリングはパフォーマンスの問題を引き起こさないのでほとんどの場合には意味がないと思ってよいです。
useCallback
フックは、MyComponent
がレンダリングされるたびに呼びされます。
useCallback
が同じ関数オブジェクトを返したとしても、再レンダリングのたびにインライン関数が再作成されます。(useCallbackはそのインライン関数を返すことをスキップするだけです。)
また、useCallback
を使うことで、コードの複雑性も増します。useCallback(..., deps)
のdepsをメモライズされたコールバックの内部で使用しているものと同期させておく必要があります。
結論として、最適化は、最適化を行わないよりもコストがかかることになります。
このような場合には、新しい関数が作成されることを許容するべきです。
まとめ
最適化を行うと、複雑さが増します。
最適化をするのが早すぎると、最適化されたコードが何度も変更される可能性があるため、リスクを伴います。
useCallbackを適切に使用するタイミングは、メモライズされた重たい子コンポーネントに供給されるコールバック関数をメモ化することです。
また、パフォーマンスの向上を数値化し、そのパフォーマンスの向上は複雑さの増加と比較して、useCallbackを使う価値があるかを判断しましょう。
#紹介
私が所属している「もりけん塾」は、もりけん先生が運営するJavaScriptに特化したコミュニティーです。(とは言ってもいろんな方がいらっしゃいます。)
フロントエンドエンジニアになりたい方にむけて、先生が道標となって初学者が迷わないように導いてくれます。コードの書き方、自走力を身に着けるにはとても良い環境です。
毎月1日に募集をかけていたのですが、ここ最近は募集かけなくても入塾したいとDMがくるそうです...!!(大人気)