0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パフォーマンス向上!useMemo と useCallback の正しい使い方

Posted at

はじめに

ReactでWebアプリケーションを構築していると、コンポーネントの再レンダリングが頻繁に発生し、パフォーマンスの問題に直面することがあります。そんな時に活用したいのが useMemouseCallback というHooksです。

しかし、「何となく使ってみたけど効果が実感できない」「過度に最適化して逆にパフォーマンスが悪化した」といった経験はありませんか?

この記事では、2025年現在のReactの動向を踏まえ、実際のコードサンプルを交えながら useMemouseCallback正しい使い方 を解説します。

useMemo と useCallback の基本概念

useMemo

useMemo計算結果をキャッシュして、依存配列が変更されない限り再計算を防ぐHooksです。

const expensiveValue = useMemo(() => {
  return heavyCalculation(data);
}, [data]);

useCallback

useCallback関数そのものをキャッシュして、不要な関数の再生成を防ぐHooksです。

const handleClick = useCallback(() => {
  doSomething(value);
}, [value]);

ポイントuseCallback(fn, deps)useMemo(() => fn, deps) と同等です。

React 19時代の変化点

React Compilerの登場で何が変わったか

2024年12月にリリースされたReact 19では、React Compilerが導入されました。これにより、多くの場合で手動のメモ化が不要になりました。

React Compilerが自動的に行うこと:

  • 不要な再レンダリングの検出とスキップ
  • 高価な計算の自動メモ化
  • 関数参照の安定化

React Compilerの導入により多くの手動メモ化が不要になりましたが、useMemouseCallback が廃止されるわけではありません。複雑な依存関係や特殊なケースでは依然として有用です。

実践的なコード例とベストプラクティス

1. 高コストな計算の最適化

配列のフィルタリングやソート、複雑な数値計算など、処理時間が1ms以上かかる高コストな計算では、useMemoを使って計算結果をキャッシュすることで、コンポーネントが再レンダリングされるたびに同じ計算を繰り返すのを防ぎます。ただし、単純な四則演算や値の参照程度の軽い処理では、メモ化のオーバーヘッドの方が大きくなるため使用を避けるべきです。

❌ 悪い例(不要なuseMemo)

const BadExample: React.FC = () => {
  const [count, setCount] = useState(0);

  // 単純な計算にuseMemoを使用 → 意味がない
  const doubledCount = useMemo(() => count * 2, [count]);

  return <div>{doubledCount}</div>;
};

✅ 良い例(適切なuseMemo)

interface Item {
  id: string;
  name: string;
  category: string;
  price: number;
}

const GoodExample: React.FC<{ items: Item[]; searchQuery: string }> = ({
  items,
  searchQuery,
}) => {
  // 計算コストが高い処理をメモ化
  const filteredAndSortedItems = useMemo(() => {
    console.time('filter-and-sort'); // 計測用

    const filtered = items.filter(item =>
      item.name.toLowerCase().includes(searchQuery.toLowerCase())
    );

    const sorted = filtered.sort((a, b) => b.price - a.price);

    console.timeEnd('filter-and-sort');
    return sorted;
  }, [items, searchQuery]);

  return (
    <ul>
      {filteredAndSortedItems.map(item => (
        <li key={item.id}>{item.name} - ${item.price}</li>
      ))}
    </ul>
  );
};

2. 子コンポーネントの再レンダリング制御

親コンポーネントが再レンダリングされると、その中で定義された関数は毎回新しいインスタンスとして生成されます。この関数をpropsとして受け取る子コンポーネントがReact.memoでメモ化されている場合、関数の参照が変わるだけで不要な再レンダリングが発生してしまいます。useCallbackを使って関数の参照を安定化させることで、子コンポーネントの不要な再レンダリングを防ぎ、パフォーマンスを改善できます。

❌ 悪い例(useCallbackなし)

const BadParent: React.FC = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 毎回新しい関数が生成される
  const handleNameChange = (newName: string) => {
    setName(newName);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* countが変わるたびにNameInputも再レンダリング */}
      <NameInput onChange={handleNameChange} />
    </div>
  );
};

const NameInput = React.memo<{ onChange: (name: string) => void }>(
  ({ onChange }) => {
    console.log('NameInput rendered'); // 毎回実行される
    return (
      <input
        onChange={(e) => onChange(e.target.value)}
        placeholder="Enter name"
      />
    );
  }
);

✅ 良い例(useCallbackで最適化)

const GoodParent: React.FC = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 関数をメモ化して再生成を防ぐ
  const handleNameChange = useCallback((newName: string) => {
    setName(newName);
  }, []); // 依存なしなので常に同じ関数

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* countが変わってもNameInputは再レンダリングされない */}
      <NameInput onChange={handleNameChange} />
    </div>
  );
};

3. useEffectの依存配列での活用

useEffectの依存配列に関数を含める場合、その関数が毎回再生成されると、エフェクトが不要に実行されてしまいます。特にAPI呼び出しなど副作用を伴う処理では、無限ループや過度なリクエストの原因となります。useCallbackで関数を安定化させることで、依存配列の変更を必要最小限に抑え、エフェクトが適切なタイミングでのみ実行されるようになります。

const DataFetcher: React.FC<{ userId: string }> = ({ userId }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  // APIクライアント設定を含む関数をメモ化
  const fetchUserData = useCallback(async (id: string) => {
    setLoading(true);
    try {
      const response = await fetch(`/api/users/${id}`, {
        headers: {
          'Authorization': `Bearer ${getToken()}`,
          'Content-Type': 'application/json'
        }
      });
      const userData = await response.json();
      setData(userData);
    } catch (error) {
      console.error('Failed to fetch user data:', error);
    } finally {
      setLoading(false);
    }
  }, []); // getToken()が安定している前提

  useEffect(() => {
    fetchUserData(userId);
  }, [userId, fetchUserData]); // useCallbackにより安定した参照

  return (
    <div>
      {loading ? 'Loading...' : data && <UserProfile data={data} />}
    </div>
  );
};

まとめ

useMemouseCallback は強力な最適化ツールですが、適材適所での使用が重要です。

使うべき場面

  • useMemo: 計算コストが高い処理(1ms以上)
  • useCallback: React.memoと組み合わせて子コンポーネントの再レンダリング防止
  • 両方: useEffectの依存配列で関数を使用する場合

使わない方が良い場面

  • 単純な計算や値の変換
  • 依存関係が複雑で管理が困難な場合
  • メモ化のコストが元の処理コストを上回る場合

「早すぎる最適化は諸悪の根源」 という格言を忘れずに、まずは正しく動作するコードを書き、本当に必要な時だけ最適化を行いましょう。

パフォーマンス計測を習慣化して、データに基づいた最適化を心がけることで、より良いReactアプリケーションを構築できます!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?