useCallback
は、再レンダリング間で関数の定義をキャッシュできる React Hook です。
const cachedFn = useCallback(fn, dependencies)
リファレンス
useCallback(fn, dependencies)
コンポーネントのトップレベルで useCallback
を呼び出して、再レンダリング間で関数の定義をキャッシュします
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
パラメータ
-
fn
: キャッシュしたい関数の値。任意の引数を受け取り、任意の値を返すことができます。React は初期レンダリング中にあなたの関数をあなたに返します(呼び出しません!)。次回のレンダリングでは、前回のレンダリング以降dependencies
が変更されていない場合、React は同じ関数を再度返します。そうでない場合は、現在のレンダリング中に渡した関数を返し、後で再利用できるように保存します。React はあなたの関数を呼び出しません。関数は、いつどのように呼び出すかを決定できるように、あなたに返されます。 -
dependencies
:fn
コード内で参照されるすべてのリアクティブ値のリスト。リアクティブ値には、props、state、およびコンポーネント本体内で直接宣言されたすべての変数と関数が含まれます。リンターが React 用に構成されている場合、すべてのリアクティブ値が依存関係として正しく指定されていることを検証します。依存関係のリストは、一定数の項目を持ち、[dep1, dep2, dep3]
のようにインラインで記述する必要があります。React は、Object.is
比較アルゴリズムを使用して、各依存関係を以前の値と比較します。
戻り値
初期レンダリングでは、useCallback
は渡された fn
関数を返します。
後続のレンダリングでは、前回のレンダリングから既に保存されている fn
関数(依存関係が変更されていない場合)を返すか、このレンダリング中に渡された fn
関数を返します。
注意点
useCallback
は Hook なので、コンポーネントのトップレベルまたは独自の Hook 内でしか呼び出すことができません。ループや条件式の中で呼び出すことはできません。そのような必要がある場合は、新しいコンポーネントを作成し、状態をそこに移動してください。- React は、特定の理由がない限り、キャッシュされた関数を破棄しません。 例えば、開発中は、コンポーネントのファイルを編集すると React はキャッシュを破棄します。開発環境と本番環境の両方で、初期マウント中にコンポーネントがサスペンドすると、React はキャッシュを破棄します。将来的には、React はキャッシュの破棄を活用する機能を追加する可能性があります。例えば、React が将来的に仮想化リストの組み込みサポートを追加する場合、仮想化テーブルのビューポートからスクロールアウトしたアイテムのキャッシュを破棄することは理にかなっています。
useCallback
をパフォーマンス最適化として使用している場合、これは期待どおりの動作です。そうでない場合は、状態変数またはrefの方が適切かもしれません。
使用方法 (SVG画像省略)
コンポーネントの再レンダリングをスキップする (SVG画像省略)
レンダリングパフォーマンスを最適化する際には、子コンポーネントに渡す関数をキャッシュする必要がある場合があります。まず、そのための構文を見て、次にどのような場合に役立つかを見てみましょう。
コンポーネントの再レンダリング間で関数をキャッシュするには、その定義を useCallback
Hook でラップします。
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallback
には2つのものを渡す必要があります。
- 再レンダリング間でキャッシュしたい関数の定義。
- 関数の内部で使用されているコンポーネント内のすべての値を含む依存関係のリスト。
初期レンダリングでは、useCallback
から返される関数は、渡した関数になります。
後続のレンダリングでは、React は依存関係を前回のレンダリングで渡した依存関係と比較します。依存関係が1つも変更されていない場合(Object.is
と比較して)、useCallback
は以前と同じ関数を返します。そうでない場合、useCallback
は*この*レンダリングで渡した関数を返します。
言い換えれば、useCallback
は、依存関係が変更されるまで、再レンダリング間で関数をキャッシュします。
例を見て、これがいつ役立つかを見てみましょう。
handleSubmit
関数を ProductPage
から ShippingForm
コンポーネントに渡しているとします。
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
theme
prop を切り替えるとアプリが一瞬フリーズすることに気づきましたが、JSX から <ShippingForm />
を削除すると、高速に感じられます。これは、ShippingForm
コンポーネントを最適化する価値があることを示しています。
デフォルトでは、コンポーネントが再レンダリングされると、React はそのすべての子を再帰的に再レンダリングします。そのため、ProductPage
が異なる theme
で再レンダリングされると、ShippingForm
コンポーネント*も*再レンダリングされます。これは、再レンダリングに多くの計算を必要としないコンポーネントにとっては問題ありません。しかし、再レンダリングが遅いことが確認された場合は、memo
: でラップすることで、前回のレンダリングと同じ props の場合に ShippingForm
に再レンダリングをスキップするように指示できます。
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
この変更により、ShippingForm
は、すべての props が前回のレンダリングと*同じ*場合に再レンダリングをスキップします。関数のキャッシュが重要になるのはこのときです! useCallback
を使用せずに handleSubmit
を定義したとしましょう。
function ProductPage({ productId, referrer, theme }) {
// Every time the theme changes, this will be a different function...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
JavaScript では、function () {}
または () => {}
は常に*異なる*関数を生成します。これは、{}
オブジェクトリテラルが常に新しいオブジェクトを作成するのと同じです。通常、これは問題になりませんが、ShippingForm
props が同じになることはなく、memo
による最適化が機能しないことを意味します。ここで useCallback
が役立ちます。
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
handleSubmit
を useCallback
でラップすることにより、再レンダリング間で*同じ*関数であることを保証します(依存関係が変更されるまで)。特定の理由がない限り、関数を useCallback
でラップする*必要はありません*。この例では、その理由は、memo
でラップされたコンポーネントに渡すことで、再レンダリングをスキップできるようにするためです。 useCallback
が必要な理由は他にもあり、このページで詳しく説明します。
詳細
子コンポーネントを最適化しようとする際に、useMemo
を useCallback
と一緒に使用することがよくあります。これらはどちらも、子コンポーネントを最適化しようとする際に役立ちます。これらを使用すると、渡しているものをメモ化(つまり、キャッシュ)できます。
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
違いは、何をキャッシュできるかです。
useMemo
は、関数を呼び出した結果をキャッシュします。 この例では、computeRequirements(product)
を呼び出した結果をキャッシュし、product
が変更されない限り、結果が変更されないようにします。これにより、ShippingForm
を不必要に再レンダリングすることなく、requirements
オブジェクトを渡すことができます。必要に応じて、React はレンダリング中に渡された関数を呼び出して結果を計算します。useCallback
は、*関数自体*をキャッシュします。useMemo
とは異なり、指定した関数を呼び出しません。代わりに、指定した関数をキャッシュして、productId
またはreferrer
が変更されない限り、handleSubmit
*自体*が変更されないようにします。これにより、ShippingForm
を不必要に再レンダリングすることなく、handleSubmit
関数を渡すことができます。ユーザーがフォームを送信するまで、コードは実行されません。
useMemo
に既に精通している場合は、useCallback
を次のように考えると役に立つかもしれません。
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
詳細
アプリがこのサイトのように、ほとんどのインタラクションが粗い(ページ全体またはセクション全体を置き換えるなど)場合、メモ化は通常不要です。一方、アプリが描画エディターのように、ほとんどのインタラクションが細かい(図形を移動するなど)場合、メモ化は非常に役立つことがあります。
useCallback
で関数をキャッシュすることは、限られた場合にのみ有効です。
memo
でラップされたコンポーネントにプロパティとして渡します。値が変更されていない場合は、再レンダリングをスキップします。メモ化により、依存関係が変更された場合にのみコンポーネントが再レンダリングされます。- 渡している関数は、後で一部のフックの依存関係として使用されます。たとえば、
useCallback
でラップされた別の関数がそれに依存している場合、またはuseEffect
からこの関数に依存している場合です。
その他の場合に useCallback
で関数をラップしてもメリットはありません。そうすることによる大きな害もないため、一部のチームは個々のケースを考慮せず、できる限りメモ化することを選択しています。欠点は、コードが読みにくくなることです。「常に新しい」単一の値は、コンポーネント全体のメモ化を壊すのに十分です。
useCallback
は関数の*作成*を妨げないことに注意してください。常に関数を作成していますが(問題ありません)、React はそれを無視し、何も変更されていない場合はキャッシュされた関数を返します。
実際には、いくつかの原則に従うことで、多くのメモ化を不要にすることができます。
- コンポーネントが視覚的に他のコンポーネントをラップする場合、JSX を子として受け入れるようにします。その後、ラッパーコンポーネントが自身の状態を更新した場合、React はその子が再レンダリングする必要がないことを認識します。
- ローカル状態を優先し、状態を必要以上にリフトアップしないでください。フォームやアイテムがツリーの最上位またはグローバル状態ライブラリでホバーされているかどうかなど、一時的な状態を保持しないでください。
- レンダリングロジックを純粋に保つ。コンポーネントの再レンダリングが問題を引き起こしたり、目に見える視覚的なアーティファクトを生成したりする場合は、コンポーネントのバグです! メモ化を追加する代わりに、バグを修正してください。
- 状態を更新する不要なエフェクトを避ける。React アプリのパフォーマンスの問題のほとんどは、コンポーネントを何度もレンダリングさせるエフェクトから発生する更新の連鎖が原因です。
- エフェクトから不要な依存関係を削除するようにしてください。たとえば、メモ化の代わりに、オブジェクトまたは関数をエフェクト内またはコンポーネント外に移動する方が簡単な場合があります。
特定のインタラクションがまだ遅れていると感じた場合は、React デベロッパーツールプロファイラを使用して、どのコンポーネントがメモ化の恩恵を最も受けているかを確認し、必要に応じてメモ化を追加します。これらの原則により、コンポーネントのデバッグと理解が容易になるため、いずれの場合も従うことをお勧めします。長期的に、メモ化を自動的に行うことを研究して、これを一度で解決しようとしています。
例 1(以下) 2: useCallbackとmemoを使った再レンダリングの回避
この例では、ShippingFormコンポーネントは**意図的に低速化**されており、レンダリングしているReactコンポーネントが実際に低速な場合に何が起こるかを確認できます。カウンターを増やしたり、テーマを切り替えたりしてみてください。
カウンターを増やすと動作が遅く感じられます。これは、低速化されたShippingFormの再レンダリングを強制するためです。これは、カウンターが変更されたため、ユーザーの新しい選択を画面に反映する必要があるため、想定どおりの動作です。
次に、テーマを切り替えてみてください。**useCallbackとmemoを組み合わせることで、意図的な低速化にもかかわらず高速です!** ShippingFormはhandleSubmit関数が変更されていないため、再レンダリングをスキップしました。 handleSubmit関数は、productIdとreferrer(useCallbackの依存関係)が前回のレンダリング以降変更されていないため、変更されていません。
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Imagine this sends a request... console.log('POST /' + url); console.log(data); }
メモ化されたコールバックからの状態の更新
メモ化されたコールバックから、以前の状態に基づいて状態を更新する必要がある場合があります。
このhandleAddTodo関数は、todosから次のtodosを計算するため、todosを依存関係として指定します。
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
通常、メモ化された関数は可能な限り少ない依存関係を持つようにします。次の状態を計算するためだけに状態を読み取る場合は、更新関数を渡すことで、その依存関係を削除できます。
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...
ここでは、todosを依存関係にして内部で読み取る代わりに、状態を*どのように*更新するかについての指示(`todos => [...todos, newTodo]`)をReactに渡します。更新関数の詳細はこちらをご覧ください。
Effectの過剰な実行を防ぐ
Effect内から関数を呼び出したい場合があります。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
これは問題を引き起こします。すべてのリアクティブ値は、Effectの依存関係として宣言する必要があります。 しかし、createOptionsを依存関係として宣言すると、Effectがチャットルームに常に再接続することになります。
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...
これを解決するには、Effectから呼び出す必要がある関数をuseCallbackでラップします。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ...
これにより、roomIdが同じであれば、再レンダリング間でcreateOptions関数が同じになることが保証されます。**ただし、関数の依存関係の必要性をなくすことがさらに望ましいです。**関数をEffectの*内部*に移動します。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ No need for useCallback or function dependencies!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
これでコードが簡素化され、useCallbackが必要なくなりました。Effectの依存関係の削除について詳しくは、こちらをご覧ください。
カスタムフックの最適化
カスタムフックを作成する場合は、返される関数をuseCallbackでラップすることをお勧めします。
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
これにより、フックの利用者が必用に応じて独自のコードを最適化できるようになります。
トラブルシューティング
コンポーネントがレンダリングされるたびに、useCallback
は異なる関数を返します
第二引数として依存関係配列を指定したことを確認してください!
依存関係配列を忘れると、useCallback
はレンダリングされるたびに新しい関数を返します
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Returns a new function every time: no dependency array
// ...
これは、依存関係配列を第二引数として渡す修正バージョンです
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Does not return a new function unnecessarily
// ...
それでも解決しない場合は、少なくとも1つの依存関係が前回のレンダリングと異なっていることが問題です。 この問題は、依存関係を手動でコンソールにログ出力することでデバッグできます
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
コンソールで異なる再レンダリングからの配列を右クリックし、両方に対して「グローバル変数として保存」を選択できます。 最初の配列が temp1
として保存され、2番目の配列が temp2
として保存されたと仮定すると、ブラウザコンソールを使用して、両方の配列の各依存関係が同じかどうかを確認できます
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
メモ化を壊している依存関係がわかったら、それを削除する方法を見つけるか、同様にメモ化します。
ループ内の各リスト項目に対して useCallback
を呼び出す必要がありますが、許可されていません
Chart
コンポーネントが memo
でラップされているとします。 ReportList
コンポーネントが再レンダリングされるときに、リスト内のすべての Chart
の再レンダリングをスキップしたいと考えています。 ただし、ループ内で useCallback
を呼び出すことはできません
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useCallback in a loop like this:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
代わりに、個々の項目のコンポーネントを抽出し、そこに useCallback
を配置します
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Call useCallback at the top level:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
または、最後のスニペットで useCallback
を削除し、代わりに Report
自体を memo
. でラップすることもできます。 item
プロパティが変更されない場合、Report
は再レンダリングをスキップするため、Chart
も再レンダリングをスキップします
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});