useCallback 完全入門【React】関数のキャッシュで再レンダリングを最適化

ReactのuseCallbackとは何か、関数をキャッシュする仕組みと使いどころを初心者向けに解説。useMemoとの違いや、memoコンポーネントとの組み合わせ方も紹介します。

#react#hooks#usecallback#javascript#frontend

「子コンポーネントに関数をpropsとして渡しているのに、毎回再レンダリングが走ってしまう……」

そんな悩みの解決策として useCallback があります。関数コンポーネントでは、レンダリングのたびに関数が新しく作られます。これが不要な再レンダリングの原因になることがあります。

この記事でわかること:

  • useCallback が必要な理由
  • 基本的な書き方とパラメータ
  • memo コンポーネントとの組み合わせパターン
  • useMemo との違い
  • 使いすぎを避けるためのガイドライン

なぜ関数のキャッシュが必要なのか?

Reactコンポーネントは、レンダリングのたびに関数を新しく作り直します

function Parent() {
  const [count, setCount] = useState(0);
 
  // ← count が変わるたびに新しい handleClick が作られる
  const handleClick = () => {
    console.log("クリック!");
  };
 
  return <Child onClick={handleClick} />;
}

handleClick は毎回「別の関数」として扱われます。もし Childmemo でラップされていても、onClick の参照が変わるため毎回再レンダリングされてしまいます。

useCallback の基本的な使い方

const cachedFn = useCallback(fn, dependencies);
パラメータ説明
fnキャッシュしたい関数
dependencies再生成のトリガーとなる値の配列
import { useCallback, useState } from "react";
 
function Parent() {
  const [count, setCount] = useState(0);
  const [theme, setTheme] = useState("light");
 
  // count が変わっても handleClick の参照が変わらない
  const handleClick = useCallback(() => {
    console.log("クリック!");
  }, []); // 依存なし → 一度だけ作成
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>カウント: {count}</button>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        テーマ切替
      </button>
      <Child onClick={handleClick} />
    </>
  );
}

memo コンポーネントとの組み合わせ

useCallback が最も効果を発揮するのは、memo でラップした子コンポーネントに関数を渡す場合です。

import { memo, useCallback, useState } from "react";
 
// memo でラップ:propsが変わらなければ再レンダリングしない
const SearchResults = memo(function SearchResults({ onItemClick }) {
  console.log("SearchResults レンダリング");
  return (
    <ul>
      <li onClick={() => onItemClick("item1")}>アイテム1</li>
      <li onClick={() => onItemClick("item2")}>アイテム2</li>
    </ul>
  );
});
 
function SearchPage() {
  const [query, setQuery] = useState("");
  const [selectedItem, setSelectedItem] = useState(null);
 
  // useCallback なしだと query が変わるたびに SearchResults も再レンダリングされる
  const handleItemClick = useCallback((item) => {
    setSelectedItem(item);
  }, []); // 依存なし
 
  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <SearchResults onItemClick={handleItemClick} />
      {selectedItem && <p>選択: {selectedItem}</p>}
    </>
  );
}

エフェクト内で使う関数のキャッシュ

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState("");
 
  // この関数を useCallback でラップすることで
  // roomId が変わったときだけ再作成される
  const createConnection = useCallback(() => {
    return connectToRoom(roomId);
  }, [roomId]);
 
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createConnection]); // createConnection が変わったときだけ再実行
}

useMemo との違い

// useCallback:関数そのものをキャッシュ
const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
 
// useMemo:関数の実行結果をキャッシュ
const result = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);
 
// useCallback(fn, deps) は useMemo(() => fn, deps) と同等
フックキャッシュするもの
useCallback関数(定義)
useMemo値(計算結果)

使いすぎに注意

useCallbackすべての関数に使う必要はありません

// ❌ 過剰:子コンポーネントに渡さない関数には不要
const handleChange = useCallback((e) => {
  setInput(e.target.value);
}, []);
 
// ✅ シンプルに書けばよい
const handleChange = (e) => {
  setInput(e.target.value);
};

効果があるのは、memo でラップした子コンポーネントへ渡す場合や、useEffect の依存配列に含める関数です。

まずコードをシンプルに書き、パフォーマンス問題が確認されてから最適化するのが正しいアプローチです。

まとめ

  • useCallback は関数の定義をキャッシュして、不要な関数の再作成を防ぐフック
  • memo コンポーネントに関数を渡す場合に効果を発揮する
  • useMemo の関数版(実行結果ではなく関数自体をキャッシュ)
  • カスタムフックが返す関数はラップすることを推奨
  • パフォーマンス問題が実際に起きてから使うのがベスト

関連記事