React 速習チュートリアル

React useCallback Hook

1. useCallback

useCallback フックは、コールバック関数を メモ化(Memoize) するために使用されます。

関数のメモ化とは、関数の結果をキャッシュすることで、再レンダリングのたびに再計算や再生成を行う必要をなくすことを指します。useCallback でラップされた関数は、その 依存関係(Dependencies) のいずれかの値が変化したときにのみ再生成されます。

これにより、リソース消費の激しい関数を隔離し、すべてのレンダリングで自動的に実行されるのを防ぐことができます。

1.1 useCallback と useMemo の違い

useCallbackuseMemo フックは似ていますが、以下の違いがあります:

  • useMemo: メモ化された「値」を返します。
  • useCallback: メモ化された「関数」を返します。

useMemo についての詳細は、useMemo の章で詳しく学習できます。

2. 構文 (Syntax)

useCallback フックは2つの引数を受け取ります。

useCallback(callback, dependencies)

  1. callback: メモ化したい関数本体。
  2. dependencies: コールバック関数が依存する値の配列。メモ化されたコールバックは、これらの依存関係が変化した場合にのみ新しく生成されます。

3. 実装例

3.1 useCallback を使用しない場合

まずは、useCallback を使用せずに実装した、最適化されていない例を見てみましょう。

// useCallbackなしの場合:
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

// 関数プロップを受け取る子コンポーネント
const Button = React.memo(({ onClick, text }) => {
  console.log(`子コンポーネント ${text} ボタンがレンダリングされました`);
  return <button onClick={onClick}>{text}</button>;
});

// useCallbackを使用しない親コンポーネント
function WithoutCallbackExample() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // これらの関数はレンダリングのたびに新しく生成されます
  const handleClick1 = () => {
    setCount1(count1 + 1);
  };

  const handleClick2 = () => {
    setCount2(count2 + 1);
  };

  console.log("親コンポーネントがレンダリングされました");
  return (
    <div>
      <h2>useCallbackなし:</h2>
      <p>カウント 1: {count1}</p>
      <p>カウント 2: {count2}</p>
      <Button onClick={handleClick1} text="ボタン 1" />
      <Button onClick={handleClick2} text="ボタン 2" />
    </div>
  );
}

createRoot(document.getElementById('root')).render(
  <WithoutCallbackExample />
);

上記の例を実行してボタンをクリックすると、ボタンをクリックするたびに3つのコンポーネントすべて(親、ボタン1、ボタン2)が再レンダリングされることに気づくでしょう。

これは、親コンポーネントが再レンダリングされるたびに handleClick1handleClick2 が新しい関数オブジェクトとして再生成されるためです。たとえ子コンポーネントを React.memo でラップしていても、プロップス(onClick)として渡される関数の参照が変わってしまうため、子コンポーネントは「新しいプロップスが届いた」と判断して再レンダリングを実行してしまいます。

3.2 useCallback を使用する場合

useCallback フックを使用することで、この問題を回避できます。関数をメモ化し、依存関係が変化したときにのみ再生成するようにします。

この最適化により、ボタン1をクリックしたときは親とボタン1だけが、ボタン2をクリックしたときは親とボタン2だけが再レンダリングされるようになります。

// useCallbackありの場合:
import React, { useState, useCallback } from 'react';
import { createRoot } from 'react-dom/client';

// 関数プロップを受け取る子コンポーネント
const Button = React.memo(({ onClick, text }) => {
  console.log(`${text} ボタンがレンダリングされました`);
  return <button onClick={onClick}>{text}</button>;
});

// useCallbackを使用した親コンポーネント
function WithCallbackExample() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // これらの関数はメモ化され、依存関係が変化したときのみ再生成されます
  const handleClick1 = useCallback(() => {
    setCount1(count1 + 1);
  }, [count1]); // count1が変化したときのみ再生成

  const handleClick2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]); // count2が変化したときのみ再生成

  console.log("親コンポーネントがレンダリングされました");
  return (
    <div>
      <h2>useCallbackあり:</h2>
      <p>カウント 1: {count1}</p>
      <p>カウント 2: {count2}</p>
      <Button onClick={handleClick1} text="ボタン 1" />
      <Button onClick={handleClick2} text="ボタン 2" />
    </div>
  );
}

createRoot(document.getElementById('root')).render(
  <WithCallbackExample />
);