はじめに
ReactでWebアプリケーションを構築していると、コンポーネントの再レンダリングが頻繁に発生し、パフォーマンスの問題に直面することがあります。そんな時に活用したいのが useMemo と useCallback というHooksです。
しかし、「何となく使ってみたけど効果が実感できない」「過度に最適化して逆にパフォーマンスが悪化した」といった経験はありませんか?
この記事では、2025年現在のReactの動向を踏まえ、実際のコードサンプルを交えながら useMemo と useCallback の 正しい使い方 を解説します。
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の導入により多くの手動メモ化が不要になりましたが、useMemo と useCallback が廃止されるわけではありません。複雑な依存関係や特殊なケースでは依然として有用です。
実践的なコード例とベストプラクティス
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>
);
};
まとめ
useMemo と useCallback は強力な最適化ツールですが、適材適所での使用が重要です。
使うべき場面
- useMemo: 計算コストが高い処理(1ms以上)
- useCallback: React.memoと組み合わせて子コンポーネントの再レンダリング防止
- 両方: useEffectの依存配列で関数を使用する場合
使わない方が良い場面
- 単純な計算や値の変換
- 依存関係が複雑で管理が困難な場合
- メモ化のコストが元の処理コストを上回る場合
「早すぎる最適化は諸悪の根源」 という格言を忘れずに、まずは正しく動作するコードを書き、本当に必要な時だけ最適化を行いましょう。
パフォーマンス計測を習慣化して、データに基づいた最適化を心がけることで、より良いReactアプリケーションを構築できます!