useCallback
リファレンス
useCallback(fn, dependencies)
コンポーネントのトップレベルで useCallback
を呼び出し、再レンダー間で関数定義をキャッシュします。
引数
fn
: キャッシュしたい関数型の値。任意の引数を取り、任意の値を返すことができます。React は初回のレンダー時にはその関数をそのまま返します(呼び出しません!)。次回以降のレンダーでは、ひとつ前のレンダー時からdependencies
が変更されていない場合、React は再び同じ関数を返します。それ以外の場合は、今回のレンダー時に渡された関数を返しつつ、後で再利用できる場合に備えて保存します。React は関数を呼び出しません。関数自体が返されるので、呼ぶか呼ばないか、いつ呼ぶのかについてはあなたが決定できます。dependencies
:fn
コード内で参照されるすべてのリアクティブな値のリストです。リアクティブな値には、props、state、コンポーネント本体に直接宣言されたすべての変数および関数が含まれます。リンタが 、すべてのリアクティブな値が依存値として正しく指定されているか確認できます。依存値のリストは要素数が一定である必要があり、[dep1, dep2, dep3]
のようにインラインで記述する必要があります。React は、 を使った比較で、それぞれの依存値を以前の値と比較します。
返り値
初回のレンダー時、useCallback
は渡された fn
関数を返します。
その後のレンダー時には、前回のレンダーからすでに保存されている fn
関数を返すか(依存配列が変更されていない場合)、このレンダー時に渡された fn
関数を返します。
注意点
useCallback
はフックですので、コンポーネントのトップレベルまたは独自のフックでのみ呼び出すことができます。ループや条件の中で呼び出すことはできません。それが必要な場合は、新しいコンポーネントを抽出し、その中にその状態を移動させてください。- React は、特定の理由がない限り、キャッシュされた関数を破棄しません。たとえば、開発環境では、コンポーネントのファイルを編集すると React はキャッシュを破棄します。開発環境と本番環境の両方で、初回マウント時にコンポーネントがサスペンドすると、React はキャッシュを破棄します。将来的に、React はキャッシュを破棄することを活用したさらなる機能を追加するかもしれません。例えば、将来的に React が仮想化リストに対する組み込みサポートを追加する場合、仮想化されたテーブルのビューポートからスクロールアウトした項目のキャッシュを破棄することが理にかなっています。これは、
useCallback
をパフォーマンスの最適化として利用する場合に期待に沿った動作となります。そうでない場合は、 や の方が適切かもしれません。
使用法
コンポーネントの再レンダーをスキップする
レンダーのパフォーマンスを最適化する際には、子コンポーネントに渡す関数をキャッシュする必要があることがあります。まずは、これを実現するための構文を見て、その後、どのような場合に便利かを見ていきましょう。
コンポーネントの再レンダー間で関数をキャッシュするには、その定義を useCallback
フックでラップします。
useCallback
には 2 つの要素を渡す必要があります。
- 再レンダー間でキャッシュしたい関数定義。
- 関数内で使用される、コンポーネント内のすべての値を含む依存値のリスト。
初回のレンダー時に useCallback
から返される関数、つまりあなたが受け取る関数は、あなたが渡した関数そのものになります。
次回以降のレンダーでは、React は依存配列を前回のレンダー時に渡した依存配列と比較します。( を使った比較で)依存配列が変更されていない場合、useCallback
は前回と同じ関数を返します。それ以外の場合、useCallback
は今回のレンダーで渡された関数を返します。
言い換えると、useCallback
は依存配列が変更されるまでの再レンダー間で関数をキャッシュします。
例を通して、これが有用な場合を見ていきましょう。
例えば、ProductPage
から ShippingForm
コンポーネントに handleSubmit
関数を渡しているとします。
theme
プロパティを切り替えるとアプリが一瞬フリーズすることに気付きましたが、JSX から <ShippingForm />
を取り除くと、高速に感じられるのだとしましょう。これは ShippingForm
コンポーネントの最適化を試みる価値があることを示してます。
デフォルトでは、コンポーネントが再レンダーされると、React はその子要素すべてを再帰的に再レンダーします。これが、ProductPage
が異なる theme
で再レンダーされると、ShippingForm
コンポーネントも再レンダーされる理由です。再レンダーに多くの計算を必要としないコンポーネントにとっては問題ありません。しかし、再レンダーが遅いことを確認できたなら、 でラップすることで、props が前回のレンダー時と同じである場合に ShippingForm
に再レンダーをスキップするように指示することができます。
この変更により、すべての props が前回のレンダー時と同じ場合、ShippingForm
は再レンダーをスキップするようになります。ここで関数のキャッシュが重要になってきます! handleSubmit
を useCallback
なしで定義したとしましょう。
JavaScript では、function () {}
または () => {}
は常に異なる関数を作成します。これは {}
のオブジェクトリテラルが常に新しいオブジェクトを作成するのと似ています。通常、これは問題になりませんが、ShippingForm
の props が決して同じにならないので による最適化は機能しなくなるということでもあります。このようなときに有用になってくるのが useCallback
です。
handleSubmit
を useCallback
でラップすることで、再レンダー間でそれを同一の関数にすることができます(依存配列が変更されるまで)。それをする特定の理由がない限り、関数を useCallback
でラップする必要はありません。今回の例における理由とは、この関数を でラップされたコンポーネントに渡せば再レンダーをスキップできるということです。このページの後半で説明されているように、useCallback
が必要な他の理由もあります。
さらに深く知る
useCallback と useMemo の関係
useCallback と useMemo の関係
useCallback
と並んで をよく見かけることでしょう。子コンポーネントを最適化しようとするとき、どちらも有用です。これらはあなたが下位に渡している何かを(言い換えると、キャッシュする)ことを可能にします。
その違いはキャッシュできる内容です。
- はあなたの関数の呼び出し結果をキャッシュします。この例では、
product
が変更されない限り、computeRequirements(product)
の呼び出し結果をキャッシュします。これにより、ShippingForm
を不必要に再レンダーすることなく、requirements
オブジェクトを下位に渡すことができます。必要に応じて、React はレンダー中にあなたが渡した関数を呼び出して結果を計算します。 useCallback
は関数自体をキャッシュします。useMemo
とは異なり、あなたが提供する関数を呼び出しません。代わりに、あなたが提供した関数をキャッシュして、productId
またはreferrer
が変更されない限り、handleSubmit
自体が変更されないようにします。これにより、ShippingForm
を不必要に再レンダーすることなく、handleSubmit
関数を下位に渡すことができます。ユーザがフォームを送信するまであなたのコードは実行されません。
すでに に詳しい場合、useCallback
を次のように考えると役立つかもしれません。
さらに深く知る
あらゆる場所に useCallback を追加すべきか?
あらゆる場所に useCallback を追加すべきか?
あなたのアプリがこのサイトのように、ほとんどのインタラクションが大まかなもの(ページ全体やセクション全体の置き換えなど)である場合、メモ化は通常不要です。一方、あなたのアプリが描画エディタのようなもので、ほとんどのインタラクションが細かなもの(図形を移動させるなど)である場合、メモ化は非常に役に立つでしょう。
useCallback
で関数をキャッシュすることが有用なのはいくつかのケースに限られます。
- それを でラップされたコンポーネントに props として渡すケース。この場合は、値が変化していない場合には再レンダーをスキップしたいでしょう。メモ化することで、依存値が異なる場合にのみコンポーネントを再レンダーさせることができます。
- あなたが渡している関数が、後で何らかのフックの依存値として使用されるケース。たとえば、他の
useCallback
でラップされた関数がそれに依存している、または からこの関数に依存しているケースです。
これらのケース以外では、関数を useCallback
でラップすることにメリットはありません。それを行っても重大な害はないため、個別のケースを考えずに、可能な限りすべてをメモ化するようにしているチームもあります。このアプローチのデメリットは、コードが読みにくくなることです。また、すべてのメモ化が効果的なわけではありません。例えば、毎回変化する値が 1 つ存在するだけで、コンポーネント全体のメモ化が無意味になってしまうこともあります。
useCallback
は関数の作成を防ぐわけではないことに注意してください。あなたは常に関数を作成しています(それは問題ありません!)。しかし、何も変わらない場合、React はそれを無視し、キャッシュされた関数を返します。
実際には、以下のいくつかの原則に従うことで、多くのメモ化を不要にすることができます。
- コンポーネントが他のコンポーネントを視覚的にラップするときは、それが。これにより、ラッパコンポーネントが自身の state を更新しても、React はその子を再レンダーする必要がないことを認識します。
- ローカル state を優先し、必要以上に を行わないようにします。フォームや、アイテムがホバーされているかどうか、といった頻繁に変化する state は、ツリーのトップやグローバルの状態ライブラリに保持しないでください。
- 保ちます。コンポーネントの再レンダーが問題を引き起こしたり、何らかの目に見える視覚的な結果を生じたりする場合、それはあなたのコンポーネントのバグです! メモ化を追加するのではなく、バグを修正します。
- 。React アプリケーションのパフォーマンス問題の大部分は、エフェクト内での連鎖的な state 更新によってコンポーネントのレンダーが何度も引き起こされるために生じます。
- 。例えば、メモ化する代わりに、オブジェクトや関数をエフェクトの中や外に移動させるだけで、簡単に解決できる場合があります。
それでも特定のインタラクションが遅いと感じる場合は、、どのコンポーネントでのメモ化が最も有効かを確認し、そこでメモ化を行いましょう。これらの原則を守ることで、コンポーネントのデバッグや理解が容易になるため、常に原則に従うことをおすすめします。長期的には、この問題を一挙に解決できるについて研究を行っています。
メモ化されたコールバックからの state 更新
場合によっては、メモ化されたコールバックから前回の state に基づいて state を更新する必要があります。
この handleAddTodo
関数は、次の todo リストを計算するために todos
を依存値として指定します。
通常、メモ化された関数からは可能な限り依存値を少なくしたいと思うでしょう。何らかの state を次の state を計算するためだけに読み込んでいる場合、代わりに を渡すことでその依存値を削除できます。
ここでは、todos
を依存値として内部で読み込む代わりに、どのように state を更新するかについての指示(todos => [...todos, newTodo]
)を React に渡します。。
エフェクトが頻繁に発火するのを防ぐ
時々、 の内部から関数を呼び出したいことがあるかもしれません。
これには問題があります。。しかし、createOptions
を依存値として宣言すると、あなたのエフェクトがチャットルームに常に再接続することになります。
これを解決するために、エフェクトから呼び出す必要がある関数を useCallback
でラップすることができます。
これにより、roomId
が同じ場合に再レンダー間で createOptions
関数が同じであることが保証されます。しかし、関数型の依存値を必要としないようにする方がさらに望ましいでしょう。関数をエフェクトの内部に移動します。
これでコードはよりシンプルになり、useCallback
が不要になりました。。
カスタムフックの最適化
あなたがを書いている場合、それが返すあらゆる関数は useCallback
でラップすることが推奨されます。
これにより、フックを利用する側が必要に応じて自身のコードを最適化することができます。
トラブルシューティング
コンポーネントがレンダーするたびに useCallback
が異なる関数を返す
第 2 引数として依存配列を指定したかを確認してください!
依存配列を忘れると、useCallback
は毎回新しい関数を返します。
以下は、第 2 引数として依存配列を渡す修正版です。
これでもうまくいかない場合、問題は、少なくとも 1 つの依存値が前回のレンダーと異なることです。依存値を手動でコンソールにログ出力することで、この問題をデバッグできます。
その後、コンソール内の異なる再レンダーからの配列を右クリックすると、それぞれに対して「グローバル変数として保存」が選択できます。最初のものが temp1
として、2 つ目が temp2
として保存されたと仮定すると、ブラウザのコンソールを使用して、両方の配列内の各依存値が同一であるかどうかを以下のように確認できます。
メモ化を壊している依存値を見つけたら、それを取り除く方法を見つけるか、または
ループ内の各リスト要素で useCallback
を呼び出す必要があるが、それは許されていない
Chart
コンポーネントが でラップされていると仮定します。ReportList
コンポーネントが再レンダーするときに、リスト内の Chart
がすべて再レンダーされてしまわないよう、一部をスキップしたいとしましょう。しかし、ループの中で useCallback
を呼び出すことはできません。
代わりに、個々のアイテムに対応するコンポーネントを抽出し、その中に useCallback
を配置します。
もしくは、最後のスニペットから useCallback
を削除し、代わりに Report
自体を でラップすることもできます。item
プロパティが変更されない場合、Report
は再レンダーをスキップするため、Chart
も再レンダーをスキップします。