[React Hooks] useCallback ガイド

[React Hooks] useCallback ガイド

公開
[React Hooks] useCallback ガイド

注: 自分の理解のため Github Copilot (Claude Sonnet 4.5) と壁打ちしながら作成した内容です。 Github Copilot による記述も多く含まれます。

また、本ページでは useEffectEvent については扱っていません。

目次


1. useCallback とは

1.1 概要

useCallback は React の Hooks の一つで、関数をメモ化(memoization)するためのフックです。

メモ化とは:

  • 計算結果をキャッシュして再利用する最適化手法
  • useCallback の場合、関数の参照を保持し、依存配列が変わらない限り同じ関数インスタンスを返す

主な用途:

  • 子コンポーネントへ渡すコールバック関数の最適化
  • useEffect や他のフックの依存配列に含める関数の安定化
  • 不要な再レンダリングの防止

1.2 基本構文

const memoizedCallback = useCallback(
  () => {
    // 関数の処理
    doSomething(a, b);
  },
  [a, b], // 依存配列
);

構成要素:

  1. コールバック関数 - メモ化したい関数
  2. 依存配列 - 関数が依存する値のリスト

1.3 依存配列の役割

依存配列は、メモ化された関数がいつ再生成されるかを制御します。

// パターン1: 空の依存配列 → 初回レンダリング時のみ関数を生成
const handleClick = useCallback(() => {
  console.log("クリックされました");
}, []);

// パターン2: 特定の値を指定 → その値が変更されたときのみ関数を再生成
const handleClick = useCallback(() => {
  console.log(`現在のカウント: ${count}`);
}, [count]);

// パターン3: 複数の値を指定 → いずれかの値が変更されたときに関数を再生成
const handleSubmit = useCallback(() => {
  submitForm(userId, formData);
}, [userId, formData]);

1.4 メモ化の仕組み

JavaScript では、関数も参照型です。コンポーネントが再レンダリングされるたびに、関数は新しく作成されます。

function Component() {
  // 再レンダリングのたびに新しい関数が作成される
  const handleClick = () => {
    console.log("クリック");
  };

  // 同じ処理でも、毎回異なる関数オブジェクト
  // handleClick !== 前回のhandleClick
}

useCallback を使うと、依存配列が変わらない限り同じ関数参照を保持できます。

function Component() {
  // 依存配列が変わらなければ同じ関数が返される
  const handleClick = useCallback(() => {
    console.log("クリック");
  }, []);

  // handleClick === 前回のhandleClick (依存配列が変わらない場合)
}

2. useCallback の動作原理

2.1 関数の再生成とメモリ参照

useCallback なしの場合

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");

  // 毎回新しい関数が作成される
  const handleClick = () => {
    console.log("クリック");
  };

  // nameが変わるだけでParentが再レンダリング
  // → handleClickも新しく生成
  // → Childも再レンダリング(React.memoを使っていても)
  // ※ Childに渡されるhandleClickの参照が変わるため
  return <Child onClick={handleClick} />;
}

useCallback ありの場合

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");

  // 依存配列が空なので、常に同じ関数参照
  const handleClick = useCallback(() => {
    console.log("クリック");
  }, []);

  // nameが変わってもhandleClickの参照は同じ
  // → React.memoと組み合わせればChildは再レンダリングされない
  return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>クリック</button>;
});

2.2 依存配列による更新タイミング

依存配列の値が変更されると、新しい関数が生成されます。

function Component() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  const calculate = useCallback(() => {
    return count * multiplier;
  }, [count, multiplier]);

  // count または multiplier が変更されたときのみ
  // calculate は新しい関数として再生成される
}

動作の流れ:

初回レンダリング: count=0, multiplier=1
→ calculate関数を生成

count=1に更新:
→ 依存配列[count, multiplier]が変化
→ calculate関数を再生成

multiplier=2に更新:
→ 依存配列[count, multiplier]が変化
→ calculate関数を再生成

他のstateが変更:
→ 依存配列は変化なし
→ 前回のcalculate関数を再利用

2.3 クロージャと依存配列

useCallback で生成された関数は、生成時の変数の値をクロージャとして保持します。

古い値を参照する問題

function Counter() {
  const [count, setCount] = useState(0);

  // ❌ 問題: 依存配列にcountがない
  const handleClick = useCallback(() => {
    console.log(count); // 常に初回の値(0)を参照
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <button onClick={handleClick}>表示</button>
    </div>
  );
}

正しい依存配列の指定

function Counter() {
  const [count, setCount] = useState(0);

  // ✅ 正しい: 依存配列にcountを含める
  const handleClick = useCallback(() => {
    console.log(count); // 最新のcountを参照
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <button onClick={handleClick}>表示</button>
    </div>
  );
}

State の更新関数形式を使う

依存配列を減らしつつ最新の値にアクセスする方法:

function Counter() {
  const [count, setCount] = useState(0);

  // ✅ より良い: 関数形式の更新で依存配列を空にできる
  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>増加</button>
    </div>
  );
}

3. useCallback を使うべき場面

3.1 子コンポーネントへの Props として渡す関数

React.memo でメモ化された子コンポーネントへ関数を渡す場合、useCallback が有効です。

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState("all");

  // ✅ useCallbackを使用
  const handleToggle = useCallback((id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo,
      ),
    );
  }, []);

  // filterが変わってもhandleToggleは再生成されない
  // → TodoItemは不要な再レンダリングを回避
  return (
    <div>
      <FilterButtons filter={filter} setFilter={setFilter} />
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
      ))}
    </div>
  );
}

// React.memoでメモ化
const TodoItem = React.memo(({ todo, onToggle }) => {
  console.log(`TodoItem ${todo.id} rendered`);
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </div>
  );
});

効果:

  • filter が変わっても handleToggle の参照は変わらない
  • TodoItemReact.memo により、Props が変わらなければ再レンダリングされない
  • 大量のリストアイテムがある場合、パフォーマンスが向上

3.2 useEffect の依存配列に含める関数

関数を useEffect の依存配列に含める場合、useCallback で安定させるべきです。

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // ✅ useCallbackでメモ化
  const fetchUser = useCallback(async () => {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
  }, [userId]);

  useEffect(() => {
    fetchUser();
  }, [fetchUser]); // fetchUserが変わったときのみ実行

  return <div>{user?.name}</div>;
}

補足: このケースでは、useEffect 内で直接関数を定義する方がシンプルなことが多いです。

useEffect(() => {
  async function fetchUser() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
  }
  fetchUser();
}, [userId]);

useCallback + useEffect 依存配列のパターンは、同じ関数を useEffect 以外の場所(ボタンの onClick など)でも使いたい場合に有用です。

useCallback なしの問題:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // ❌ 毎回新しい関数が生成される
  const fetchUser = async () => {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
  };

  useEffect(() => {
    fetchUser();
  }, [fetchUser]); // 毎回実行される(無限ループの可能性)

  return <div>{user?.name}</div>;
}

3.3 useMemo や他のフックの依存配列に含める関数

function DataProcessor({ data }) {
  const [threshold, setThreshold] = useState(10);

  // ✅ フィルタ関数をメモ化
  const filterData = useCallback(
    (items) => {
      return items.filter((item) => item.value > threshold);
    },
    [threshold],
  );

  // filterDataが変わったときのみ再計算
  const processedData = useMemo(() => {
    const filtered = filterData(data);
    return filtered.map((item) => ({
      ...item,
      processed: true,
    }));
  }, [data, filterData]);

  return <DataView data={processedData} />;
}

3.4 カスタムフックから返す関数

カスタムフックから関数を返す場合、useCallback でメモ化するのがベストプラクティスです。

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  // ✅ 返す関数をメモ化
  const setValue = useCallback(
    (value) => {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    },
    [key],
  );

  // ✅ 返す関数をメモ化
  const removeValue = useCallback(() => {
    setStoredValue(initialValue);
    window.localStorage.removeItem(key);
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

// 使用側
function Component() {
  const [name, setName, removeName] = useLocalStorage("name", "");

  // setNameの参照が安定しているため、useEffectの依存配列に安心して含められる
  useEffect(() => {
    // 何らかの処理
  }, [setName]);
}

4. useCallback を使うべきでない場面

4.1 通常のイベントハンドラ

子コンポーネントに渡さない、通常のイベントハンドラには不要です。

// ❌ 不要: 単純なイベントハンドラにuseCallbackは過剰
function Form() {
  const [name, setName] = useState("");

  const handleChange = useCallback((e) => {
    setName(e.target.value);
  }, []);

  return <input value={name} onChange={handleChange} />;
}
// ✅ これで十分
function Form() {
  const [name, setName] = useState("");

  const handleChange = (e) => {
    setName(e.target.value);
  };

  return <input value={name} onChange={handleChange} />;
}

理由:

  • 関数の再生成コストは非常に小さい
  • useCallback 自体にもオーバーヘッドがある(依存配列の比較処理など)
  • <input> 等の DOM 要素は React.memo でラップされていないため、関数参照が変わっても追加の再レンダリングコストは発生しない

4.2 依存が頻繁に変わる関数

依存配列が頻繁に変わる場合、useCallback の効果が薄れます。

function SearchResults({ query, filters, sortBy, page }) {
  // ❌ 依存が多く、頻繁に変わる
  const fetchResults = useCallback(async () => {
    const response = await fetch(
      `/api/search?q=${query}&filters=${filters}&sort=${sortBy}&page=${page}`,
    );
    return response.json();
  }, [query, filters, sortBy, page]);

  // この場合、useCallbackの効果は限定的
  // 依存が変わるたびに新しい関数が生成される
}

4.3 シンプルな関数

非常にシンプルな関数は、メモ化のコストの方が高い場合があります。

// ❌ 過剰: 単純すぎる関数
function ComponentA() {
  const add = useCallback((a, b) => a + b, []);
}
// ✅ これで十分
function ComponentB() {
  const add = (a, b) => a + b;
}

4.4 パフォーマンス問題がない場面

計測して問題がない限り、最適化は不要です。

// ❌ 過剰な最適化
function SmallList({ items }) {
  const handleClick = useCallback((id) => {
    console.log(id);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}
// ✅ シンプルに(アイテムが10個程度なら最適化は不要)
function SmallList({ items }) {
  const handleClick = (id) => {
    console.log(id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

最適化の原則:

  1. まず動くコードを書く
  2. パフォーマンス問題を計測する
  3. 問題がある箇所のみ最適化する

5. 注意点とベストプラクティス

5.1 依存配列の正確な指定

依存配列を正確に指定しないと、古い値を参照するバグが発生します。

// ❌ 問題: queryが依存配列にない
function SearchFormBad() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = useCallback(async () => {
    const data = await searchAPI(query); // 初回の空文字列しか参照できない
    setResults(data);
  }, []);
}
// ✅ 正しい: queryを依存配列に含める
function SearchFormGood() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = useCallback(async () => {
    const data = await searchAPI(query);
    setResults(data);
  }, [query]);
}

5.2 React.memo との組み合わせ

useCallback の効果を最大化するには、子コンポーネントを React.memo でラップします。

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = useCallback(() => {
    console.log("クリック");
  }, []);

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      {/* textが変わってもChildButton は再レンダリングされない */}
      <ChildButton onClick={handleClick} />
    </div>
  );
}

// ✅ React.memoでラップ
const ChildButton = React.memo(({ onClick }) => {
  console.log("ChildButton rendered");
  return <button onClick={onClick}>クリック</button>;
});

React.memo なしの場合:

// ❌ React.memoなし
const ChildButton = ({ onClick }) => {
  console.log("ChildButton rendered");
  return <button onClick={onClick}>クリック</button>;
};

// useCallbackを使っても、親が再レンダリングされると
// 子も再レンダリングされる(React.memoがないため)

5.3 過度な最適化を避ける

すべての関数に useCallback を使うのは過剰です。

// ❌ やりすぎ
function Component() {
  const handleClick = useCallback(() => {}, []);
  const handleChange = useCallback(() => {}, []);
  const handleSubmit = useCallback(() => {}, []);
  const handleReset = useCallback(() => {}, []);
  // ...
}

最適化のガイドライン:

  1. デフォルトは最適化しない - 通常の関数として実装
  2. 計測する - React DevTools Profiler で実際のパフォーマンスを確認
  3. 必要な箇所のみ最適化 - ボトルネックが特定された場合のみ useCallback を追加

5.4 ESLint ルールの活用

eslint-plugin-react-hooks を使うと、依存配列の漏れを検出できます。

// .eslintrc.json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

例:

function Component() {
  const [count, setCount] = useState(0);

  // ESLintが警告: countを依存配列に含めるべき
  const handleClick = useCallback(() => {
    console.log(count);
  }, []); // ⚠️ 警告: React Hook useCallback has a missing dependency: 'count'
}

6. 実践的な例

6.1 リスト項目のコールバック

大量のリストアイテムがある場合の最適化:

function TodoApp() {
  const [todos, setTodos] = useState([
    /* 大量のTodo */
  ]);

  // ✅ 各アクションをメモ化
  const handleToggle = useCallback((id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);

  const handleEdit = useCallback((id, newText) => {
    setTodos((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, text: newText } : todo)),
    );
  }, []);

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
          onEdit={handleEdit}
        />
      ))}
    </div>
  );
}

// React.memoでメモ化
const TodoItem = React.memo(({ todo, onToggle, onDelete, onEdit }) => {
  console.log(`TodoItem ${todo.id} rendered`);

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>削除</button>
      <button onClick={() => onEdit(todo.id, prompt("新しいテキスト"))}>
        編集
      </button>
    </div>
  );
});

6.2 デバウンス処理との組み合わせ

検索入力などでデバウンスを実装する場合:

基本的な実装

function SearchInput() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  // ✅ デバウンス処理をメモ化(クリーンアップ付き)
  const debouncedSearch = useCallback((searchQuery) => {
    const timeoutId = setTimeout(async () => {
      if (searchQuery) {
        const data = await fetch(`/api/search?q=${searchQuery}`).then((r) =>
          r.json(),
        );
        setResults(data);
      } else {
        setResults([]);
      }
    }, 500);

    // クリーンアップ関数を返す
    return () => clearTimeout(timeoutId);
  }, []);

  useEffect(() => {
    const cleanup = debouncedSearch(query);
    return cleanup;
  }, [query, debouncedSearch]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <SearchResults results={results} />
    </div>
  );
}

より実用的な useDebounce カスタムフック

デバウンス処理を再利用可能なカスタムフックとして実装:

// デバウンスされた値を返すフック
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 値が変わったらタイマーをセット
    const timeoutId = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // クリーンアップでタイマーをクリア
    return () => clearTimeout(timeoutId);
  }, [value, delay]);

  return debouncedValue;
}

// 使用例
function SearchInput() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  // ✅ デバウンスされた検索クエリ
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((r) => r.json())
        .then((data) => setResults(data));
    } else {
      setResults([]);
    }
  }, [debouncedQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <SearchResults results={results} />
    </div>
  );
}

デバウンスされたコールバックのカスタムフック

関数をデバウンスしたい場合:

function useDebouncedCallback(callback, delay) {
  const callbackRef = useRef(callback);
  const timeoutRef = useRef(null);

  // 最新のコールバックを保持
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // クリーンアップ
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  // デバウンスされたコールバックを返す
  return useCallback(
    (...args) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    },
    [delay],
  );
}

// 使用例
function SearchInput() {
  const [results, setResults] = useState([]);

  const performSearch = async (searchQuery) => {
    if (searchQuery) {
      const data = await fetch(`/api/search?q=${searchQuery}`).then((r) =>
        r.json(),
      );
      setResults(data);
    }
  };

  // ✅ デバウンスされた検索関数
  const debouncedSearch = useDebouncedCallback(performSearch, 500);

  return (
    <div>
      <input
        onChange={(e) => debouncedSearch(e.target.value)}
        placeholder="検索..."
      />
      <SearchResults results={results} />
    </div>
  );
}

ポイント:

  • クリーンアップ処理: コンポーネントのアンマウント時やクエリ変更時に、保留中のタイマーをクリア
  • Ref の活用: 最新のコールバックを参照しつつ、デバウンス関数の参照を安定させる
  • 再利用性: カスタムフックにすることで、複数の場所で同じロジックを使える

6.3 カスタムフックでの使用

API フェッチのカスタムフック:

function useApi(endpoint) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // ✅ fetch関数をメモ化
  const fetchData = useCallback(
    async (params = {}) => {
      setLoading(true);
      setError(null);

      try {
        const queryString = new URLSearchParams(params).toString();
        const url = queryString ? `${endpoint}?${queryString}` : endpoint;
        const response = await fetch(url);

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();
        setData(result);
        return result;
      } catch (err) {
        setError(err.message);
        throw err;
      } finally {
        setLoading(false);
      }
    },
    [endpoint],
  );

  // ✅ reset関数もメモ化
  const reset = useCallback(() => {
    setData(null);
    setError(null);
    setLoading(false);
  }, []);

  return { data, loading, error, fetchData, reset };
}

// 使用例
function UserProfile({ userId }) {
  const {
    data: user,
    loading,
    error,
    fetchData,
  } = useApi(`/api/users/${userId}`);

  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchDataが安定しているため安全

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={() => fetchData()}>再読み込み</button>
    </div>
  );
}

7. useMemo との違い

7.1 基本的な違い

useCallback:

  • 関数そのものをメモ化
  • 関数の参照を保持
const memoizedFunction = useCallback(() => {
  return a + b;
}, [a, b]);

useMemo:

  • 関数の実行結果(値) をメモ化
  • 計算結果を保持
const memoizedValue = useMemo(() => {
  return a + b;
}, [a, b]);

7.2 使い分けの基準

function Example() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  // ✅ useCallback: 関数を返したい
  const add = useCallback(() => {
    return a + b;
  }, [a, b]);

  // add は関数
  const result1 = add(); // 実行が必要

  // ✅ useMemo: 値を返したい
  const sum = useMemo(() => {
    return a + b;
  }, [a, b]);

  // sum は既に計算された値
  const result2 = sum; // そのまま使える

  return (
    <div>
      {/* 関数を子に渡す */}
      <Child onCalculate={add} />
      {/* 値を表示 */}
      <p>合計: {sum}</p>
    </div>
  );
}

変換の関係:

// これら二つは等価
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);
// useMemoで関数を返すことでuseCallbackと同じ効果

使い分けのポイント:

用途使用するフック理由
コールバック関数をメモ化useCallback関数の参照を保持するため
高コストな計算結果をメモ化useMemo計算結果の値を保持するため
オブジェクトや配列をメモ化useMemo値(参照)を保持するため
関数を useEffect の依存配列に含むuseCallback意図が明確で可読性が高い
JSX のメモ化useMemoJSX(要素)を返すため

8. React 19 / React Compiler との関係

8.1 React Compiler の概要

React Compiler(以前の React Forget)は、React 19 以降を対象とした別途導入するビルドツール(Babel プラグイン)です。React 19 に同梱されているわけではなく、プロジェクトに個別にインストールして使用します。導入することで、多くの場合で手動の useCallbackuseMemo が不要になります。

React Compiler の特徴:

  • コンポーネントとフックを自動的に最適化
  • 適切な場所に自動的にメモ化を挿入
  • 開発者が手動で最適化する必要性を削減
  • React 19 以降が必要(React 18 以前では使用不可)

8.2 React Compiler 使用時の useCallback

// React Compiler が有効な場合、これらは自動的に最適化される
function Component() {
  const [count, setCount] = useState(0);

  // ✅ React Compiler が自動的に最適化
  // 手動で useCallback を使う必要がない
  const handleClick = () => {
    console.log(count);
  };

  return <ChildComponent onClick={handleClick} />;
}

8.3 手動最適化が依然として必要なケース

React Compiler を使用しても、以下の場合は手動最適化が必要になる可能性があります。

  1. 外部ライブラリとの統合
    • サードパーティライブラリが特定の参照の安定性を要求する場合
  2. 複雑な最適化ロジック
    • カスタムな最適化戦略が必要な場合
  3. 段階的な移行
    • React Compiler を導入する前の既存コードベース

8.4 移行戦略

// 段階的な移行の例

// フェーズ1: 既存のコードベース(useCallback を使用)
function ComponentV1() {
  const handleClick = useCallback(() => {
    console.log("クリック");
  }, []);
  return <Button onClick={handleClick} />;
}

// フェーズ2: React Compiler 導入後(シンプルに)
function ComponentV2() {
  const handleClick = () => {
    console.log("クリック");
  };
  return <Button onClick={handleClick} />;
}

推奨アプローチ:

  • 新規プロジェクト: React Compiler を有効にして、手動最適化は最小限に
  • 既存プロジェクト: 段階的に React Compiler を導入し、既存の useCallback は動作確認しながら徐々に削除
  • パフォーマンス検証: React DevTools Profiler で実際の効果を確認

9. useRef を使った高度なパターン

9.1 最新の値を参照しつつ依存配列を最小化

useRef を活用すると、依存配列を空にしつつ常に最新の値を参照できます。

function Component({ onEvent, data }) {
  // ✅ Ref に最新の値を保存
  const onEventRef = useRef(onEvent);
  const dataRef = useRef(data);

  // 常に最新の値で Ref を更新
  useEffect(() => {
    onEventRef.current = onEvent;
    dataRef.current = data;
  });

  // 依存配列は空だが、常に最新の値を参照できる
  const handleClick = useCallback(() => {
    onEventRef.current(dataRef.current);
  }, []); // 空の依存配列

  return <button onClick={handleClick}>クリック</button>;
}

9.2 カスタムフックで抽象化

このパターンをカスタムフックにすると再利用しやすくなります。

// 最新の値を参照する関数を作成するフック
function useLatestCallback(callback) {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  });

  return useCallback((...args) => {
    return callbackRef.current(...args);
  }, []);
}

// 使用例
function SearchComponent({ onSearch, filters }) {
  // ✅ onSearchやfiltersが変わっても handleSearch の参照は変わらない
  const handleSearch = useLatestCallback(() => {
    onSearch(filters);
  });

  // handleSearchの参照が安定しているため、依存配列に安心して含められる
  useEffect(() => {
    // 何らかの初期化処理
    handleSearch();
  }, [handleSearch]);

  return <button onClick={handleSearch}>検索</button>;
}

9.3 使い分けのガイドライン

パターンメリットデメリット使用場面
useCallbackシンプルで理解しやすい依存配列の管理が必要一般的なメモ化
useRef + useCallback依存配列を最小化できるやや複雑頻繁に変わる値を参照する場合
useLatestCallback再利用可能で保守性が高い追加の抽象化レイヤー複数箇所で同じパターンを使う場合

10. まとめ

  • 関数の参照を安定させるパフォーマンス最適化ツール(依存配列が変わらない限り同じインスタンスを返す)
  • 必須ではない(デフォルトは最適化せず、パフォーマンス問題を計測してから使う)
  • React Compiler: React 19 以降では自動最適化により手動使用が減少する見込み
  • 使うべき場面:
    • React.memo された子コンポーネントへ渡す関数
    • useEffect や他のフックの依存配列に含める関数
    • カスタムフックから返す関数
  • 避けるべき場面:
    • 通常のイベントハンドラ(子に渡さない場合)
    • 依存が頻繁に変わる関数
    • 非常にシンプルな関数
  • ベストプラクティス:
    • 依存配列を正確に指定(ESLint ルールを活用)
    • React.memo と組み合わせて効果を最大化
    • 関数形式の state 更新で依存配列を減らす
    • 過度な使用は避ける(コードの複雑化を防ぐ)
    • 高度な場合は useRef パターンを検討
// 最適化の順序
// 1. まず動くコードを書く
function Component() {
  const handleClick = () => console.log("クリック");
  return <button onClick={handleClick}>クリック</button>;
}

// 2. パフォーマンス問題を計測

// 3. 必要な箇所のみ最適化
function Component() {
  const handleClick = useCallback(() => console.log("クリック"), []);
  return <MemoizedButton onClick={handleClick} />;
}