Open9

useCallback

あおけんあおけん

リファレンス

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 関数を返す。

注意点

1つ目
useCallback はフックですので、コンポーネントのトップレベルまたは独自のフックでのみ呼び出すことができる。
ループや条件の中で呼び出すことはできない。
それが必要な場合は、新しいコンポーネントを抽出し、その中にその状態を移動する。

2つ目
React は、特定の理由がない限り、キャッシュされた関数を破棄しない。
たとえば、開発環境では、コンポーネントのファイルを編集すると React はキャッシュを破棄される。
開発環境と本番環境の両方で、初回マウント時にコンポーネントがサスペンドすると、React はキャッシュを破棄。
将来的に、React はキャッシュを破棄することを活用したさらなる機能を追加するかもしれない。
例えば、将来的に React が仮想化リストに対する組み込みサポートを追加する場合、仮想化されたテーブルのビューポートからスクロールアウトした項目のキャッシュを破棄することが理にかなっている。
これは、useCallback をパフォーマンスの最適化として利用する場合に期待に沿った動作となる。そうでない場合は、state 変数 や ref の方が適切かもしれない。

あおけんあおけん

コンポーネントの再レンダーをスキップする

レンダーのパフォーマンスを最適化する際には、子コンポーネントに渡す関数をキャッシュする必要があることがある。
まずは、これを実現するための構文を見て、その後、どのような場合に便利かを見る。

コンポーネントの再レンダー間で関数をキャッシュするには、その定義を useCallback フックでラップする。

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
  // ...

useCallback には 2 つの要素を渡す必要がある。

  1. 再レンダー間でキャッシュしたい関数定義
  2. 関数内で使用される、コンポーネント内のすべての値を含む依存値のリスト

次回以降のレンダーでは、React は依存配列を前回のレンダー時に渡した依存配列と比較する。(Object.is を使った比較で)
依存配列が変更されていない場合、useCallback は前回と同じ関数を返す。
それ以外の場合、useCallback は今回のレンダーで渡された関数を返す。
言い換えると、useCallback は依存配列が変更されるまでの再レンダー間で関数をキャッシュする。

例を通して、これが有用な場合を見ていきましょう
例えば、ProductPage から ShippingForm コンポーネントに handleSubmit 関数を渡しているとする。

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

theme プロパティを切り替えるとアプリが一瞬フリーズすることに気付きましたが、JSX から <ShippingForm /> を取り除くと、高速に感じられるのだとする。
これは ShippingForm コンポーネントの最適化を試みる価値があることを示している。

デフォルトでは、コンポーネントが再レンダーされると、React はその子要素すべてを再帰的に再レンダーします。
これが、ProductPage が異なる theme で再レンダーされると、ShippingForm コンポーネントも再レンダーされる理由です。
再レンダーに多くの計算を必要としないコンポーネントにとっては問題ありません。
しかし、再レンダーが遅いことを確認できたなら、memo でラップすることで、props が前回のレンダー時と同じである場合に ShippingForm に再レンダーをスキップするように指示することができます。

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

この変更により、すべての props が前回のレンダー時と同じ場合、ShippingForm は再レンダーをスキップするようになります。
ここで関数のキャッシュが重要になってきます!
handleSubmit を useCallback なしで定義したとしましょう。

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 が必要な他の理由もあります。

あおけんあおけん

useCallback と useMemo の関係

useCallback と並んで useMemo をよく見かけることでしょう。
子コンポーネントを最適化しようとするとき、どちらも有用です。
これらはあなたが下位に渡している何かをメモ化する (memoize)(言い換えると、キャッシュする)ことを可能にします。

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>
  );
}

その違いはキャッシュできる内容です。
1つ目
useMemo はあなたの関数の呼び出し結果をキャッシュします。
この例では、product が変更されない限り、computeRequirements(product) の呼び出し結果をキャッシュします。
これにより、ShippingForm を不必要に再レンダーすることなく、requirements オブジェクトを下位に渡すことができます。
必要に応じて、React はレンダー中にあなたが渡した関数を呼び出して結果を計算します。
2つ目
useCallback は関数自体をキャッシュします。
useMemo とは異なり、あなたが提供する関数を呼び出しません。
代わりに、あなたが提供した関数をキャッシュして、productId または referrer が変更されない限り、handleSubmit 自体が変更されないようにします。
これにより、ShippingForm を不必要に再レンダーすることなく、handleSubmit 関数を下位に渡すことができます。
ユーザがフォームを送信するまであなたのコードは実行されません。

すでに useMemo に詳しい場合、useCallback を次のように考えると役立つかもしれません。

// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
あおけんあおけん

あらゆる場所に useCallback を追加すべきか?

あなたのアプリがこのサイトのように、ほとんどのインタラクションが大まかなもの(ページ全体やセクション全体の置き換えなど)である場合、メモ化は通常不要です。
一方、あなたのアプリが描画エディタのようなもので、ほとんどのインタラクションが細かなもの(図形を移動させるなど)である場合、メモ化は非常に役に立つでしょう。

useCallback で関数をキャッシュすることが有用なケース

1つ目
それを memo でラップされたコンポーネントに props として渡すケース。
この場合は、値が変化していない場合には再レンダーをスキップしたいでしょう。
メモ化することで、依存値が異なる場合にのみコンポーネントを再レンダーさせることができます。
2つ目
あなたが渡している関数が、後で何らかのフックの依存値として使用されるケース。
たとえば、他の useCallback でラップされた関数がそれに依存している、または useEffect からこの関数に依存しているケースです。

これらのケース以外では、関数を useCallback でラップすることにメリットはありません。
それを行っても重大な害はないため、個別のケースを考えずに、可能な限りすべてをメモ化するようにしているチームもあります。
このアプローチのデメリットは、コードが読みにくくなることです。
また、すべてのメモ化が効果的なわけではありません。
例えば、毎回変化する値が 1 つ存在するだけで、コンポーネント全体のメモ化が無意味になってしまうこともあります。

useCallback は関数の作成を防ぐわけではないことに注意してください。
あなたは常に関数を作成しています(それは問題ありません!)。
しかし、何も変わらない場合、React はそれを無視し、キャッシュされた関数を返します。

メモ化が不要なケース

1つ目
コンポーネントが他のコンポーネントを視覚的にラップするときは、それが子として JSX を受け入れるようにします。
これにより、ラッパコンポーネントが自身の state を更新しても、React はその子を再レンダーする必要がないことを認識します。
2つ目
ローカル state を優先し、必要以上に state のリフトアップを行わないようにします。
フォームや、アイテムがホバーされているかどうか、といった頻繁に変化する state は、ツリーのトップやグローバルの状態ライブラリに保持しないでください。
3つ目
レンダーロジックを純粋に保ちます。
コンポーネントの再レンダーが問題を引き起こしたり、何らかの目に見える視覚的な結果を生じたりする場合、それはあなたのコンポーネントのバグです!
メモ化を追加するのではなく、バグを修正します。
4つ目
state を更新する不要なエフェクトを避けてください。
React アプリケーションのパフォーマンス問題の大部分は、エフェクト内での連鎖的な state 更新によってコンポーネントのレンダーが何度も引き起こされるために生じます。
5つ目
エフェクトから不要な依存値をできるだけ削除します。
例えば、メモ化する代わりに、オブジェクトや関数をエフェクトの中や外に移動させるだけで、簡単に解決できる場合があります。

それでも特定のインタラクションが遅いと感じる場合は、React Developer Tools のプロファイラを使用して、どのコンポーネントでのメモ化が最も有効かを確認し、そこでメモ化を行いましょう。
これらの原則を守ることで、コンポーネントのデバッグや理解が容易になるため、常に原則に従うことをおすすめします。
長期的には、この問題を一挙に解決できる自動的なメモ化について研究を行っています。

あおけんあおけん

メモ化されたコールバックからの state 更新

場合によっては、メモ化されたコールバックから前回の state に基づいて state を更新する必要があります。
この handleAddTodo 関数は、次の todo リストを計算するために todos を依存値として指定します。

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

通常、メモ化された関数からは可能な限り依存値を少なくしたいと思うでしょう。
何らかの state を次の state を計算するためだけに読み込んでいる場合、代わりに更新用関数 (updater function) を渡すことでその依存値を削除できます。

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 を依存値として内部で読み込む代わりに、どのように state を更新するかについての指示(todos => [...todos, newTodo])を React に渡します。

あおけんあおけん

エフェクトが頻繁に発火するのを防ぐ

時々、エフェクト の内部から関数を呼び出したいことがあるかもしれません。

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();
    // ...

これには問題があります。
すべてのリアクティブな値はエフェクトの依存値として宣言されなければなりません。
しかし、createOptions を依存値として宣言すると、あなたのエフェクトがチャットルームに常に再接続することになります。

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 Problem: This dependency changes on every render
  // ...

これを解決するために、エフェクトから呼び出す必要がある関数を 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 関数が同じであることが保証されます。
しかし、関数型の依存値を必要としないようにする方がさらに望ましいでしょう。
関数をエフェクトの内部に移動します。
これでコードはよりシンプルになり、useCallback が不要になりました。

あおけんあおけん

カスタムフックの最適化

あなたがカスタムフックを書いている場合、それが返すあらゆる関数は 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 が異なる関数を返す

第 2 引数として依存配列を指定したかを確認してください!
依存配列を忘れると、useCallback は毎回新しい関数を返します。

function ProductPage({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }); // 🔴 Returns a new function every time: no dependency array
  // ...

以下は、第 2 引数として依存配列を渡す修正版です。

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>
  );
});