React.useCallback()を使うとき、使わないとき

どんな状況であっても、すべてのコールバック関数を useCallback() の中に入れることに関して少し考察したい。

 

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {    // handle the click event  }, []);
  return <MyChild onClick={handleClick} />;
}

 

つまり、コールバック関数を使用する子コンポーネントの無駄な再レンダリングを防ぐために、すべてのコールバック関数をメモ化する必要がある」という考え方ですが、

 

この推論は真実からかけ離れていると思ってまして、さらに、このようなuseCallback()の使い方をすると、コンポーネントの動作が遅くなると思います。

 

今回の記事では、正しいuseCallback()の使い方を説明します。

 

関数の理解 等価性チェック

 

useCallback()の使い方に入る前に、useCallback()が解決する問題、つまり関数の等質性チェックを区別しておきましょう。

 

関数を返す関数 factory() を書いてみましょう。

 

function factory() {
  return (a, b) => a + b;
}

const sum1 = factory();
const sum2 = factory();

sum1(1, 2); // => 3
sum2(1, 2); // => 3

sum1 === sum2; // => falsesum1 === sum1; // => true

 

sum1とsum2は、2つの数値を合計する関数です。これらは factory() 関数によって作成されています。

 

JavaScriptの関数は第一級でして、つまり、関数は通常のオブジェクトでして、

 

関数オブジェクトは、factory()が行うように、他の関数から返されたり、比較されたりなど、オブジェクトでできることは何でもできます。

 

関数sum1とsum2は同じコードソースを共有していますが、それぞれ異なる関数オブジェクトです。これらを比較すると、sum1 === sum2 は false と評価されます。

 

これがJavaScriptのオブジェクトの仕組みです。オブジェクト(関数オブジェクトを含む)は、それ自身としか等しくありません。

 

useCallback()の目的

 

同じコードを共有する異なるfunction オブジェクトは、しばしばReactコンポーネント内に作成されます。

 

function MyComponent() {
  // handleClick is re-created on each render
  const handleClick = () => {    console.log('Clicked!');  };
  // ...
}

 

handleClickは、MyComponentのレンダリングごとに異なる関数オブジェクトです。

 

インライン関数は安価なので、レンダリングごとに関数を再作成しても問題ありません。コンポーネントごとに数個のインライン関数があれば問題ありません。

 

しかし、場合によっては、下記のようにレンダリング間で1つの関数インスタンスを維持する必要があります。

 

  • React.memo()でラップされたfunction コンポーネントは、関数オブジェクトpropを受け取ります。
  • useEffect(…, [callback])のように、関数オブジェクトが他のフックに依存している場合。
  • 関数が何らかの内部状態を持っているとき、例えば関数がデバウンスやスロットルされているとき。

useCallback(callbackFun, deps)が役に立つのはこのときです。

 

同じ依存関係の値depsが与えられると、フックはレンダリングの間に関数インスタンスを返します(別名memoize)。

 

import { useCallback } from 'react';

function MyComponent() {
  // handleClick is the same function object
  const handleClick = useCallback(() => {    console.log('Clicked!');  }, []);
  // ...
}

 

handleClick変数は、MyComponentのレンダリングの間、常に同じコールバック関数オブジェクトを持っています。

 

良い使用例

 

アイテムの大きなリストをレンダリングするコンポーネントがあるとします。

 

import useSearch from './fetch-items';

function 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()でラップしています。

 

MyBigListの親コンポーネントは、アイテムがクリックされたことを知るためのハンドラ関数を提供しています。

 

import { useCallback } from 'react';

export default function MyParent({ term }) {
  const onItemClick = useCallback(event => {
    console.log('You clicked ', event.currentTarget);
  }, [term]);

  return (
    <MyBigList
      term={term}
      onItemClick={onItemClick}
    />
  );
}

 

onItemClickコールバックは、useCallback()によってメモされます。term が同じであれば、useCallback() は同じ関数オブジェクトを返します。

 

MyParentコンポーネントが再レンダリングされても、onItemClick関数オブジェクトは同じままで、MyBigListのメモ化が壊れることはありません。

 

これはuseCallback()の良い使用例です。

 

悪いユースケース

 

別の例を見てみましょう。

 

import { useCallback } from 'react';

function MyComponent() {
  // Contrived use of `useCallback()`
  const handleClick = useCallback(() => {    // handle the click event  }, []);
  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

 

useCallback()を適用することに意味はあるのでしょうか?<MyChild>コンポーネントは軽く、その再レンダリングはパフォーマンスの問題を引き起こさないので、ほとんどの場合は意味がありません。

 

useCallback()フックは、MyComponentがレンダリングするたびに呼び出されることを忘れないでください。

 

useCallback() が同じ関数オブジェクトを返したとしても、再レンダリングのたびにインライン関数が再作成されます (useCallback() はそれをスキップするだけです)。

 

useCallback()を使うことで、コードの複雑さも増します。useCallback(…, deps)の deps を、

 

メモライズされたコールバックの内部で使用しているものと同期させておく必要があります。

 

結論として、最適化は、最適化を行わないよりもコストがかかります。

 

再レンダリングのたびに新しい関数が作成されることを受け入れてください。

 

import { useCallback } from 'react';

function MyComponent() {
  const handleClick = () => {    // handle the click event  };
  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

 

おわりに

 

最適化を行うと、複雑さが増します。最適化が早すぎると、最適化されたコードが何度も変更される可能性があるため、リスクを伴います。

 

これらの考慮事項は、useCallback()フックにも当てはまります。その適切な使用例は、メモライズされた重い子コンポーネントに供給されるコールバック関数をメモライズすることです。

 

コンポーネント出力全体のメモ化を有効にするには、私の投稿Use React.memo()wiselyをチェックすることをお勧めします。

 

参照

藤沢瞭介(Ryosuke Hujisawa)
  • りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

未整理記事

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です