Reactの基礎 - useMemo
概要
useMemo は、Reactの関数コンポーネントで計算結果をメモ化 (キャッシュ) するためのHookである。
コンポーネントが再レンダリングされるたびに重い計算が実行されると、パフォーマンスが低下する場合がある。
useMemo を使用すると、依存配列の値が変化しない限り前回の計算結果を再利用できるため、不要な再計算を防止できる。
基本構文は useMemo(calculateValue, dependencies) であり、第1引数に値を計算する関数、第2引数に依存配列を指定する。
依存配列の各要素は Object.is() で比較され、値が変化した場合のみ計算関数が再実行される。
useMemo が特に有効な場面は以下に示す3つである。
- 計算処理が明らかに重い場合 (数百〜数千要素の配列処理等)
- メモ化した値を
React.memoでラップされた子コンポーネントにpropとして渡す場合 - メモ化した値が他のHook (
useEffect等) の依存配列に含まれる場合
React Compiler (2025年10月 v1.0リリース) は、コンポーネント内の計算を自動的にメモ化する最適化を行う。
React Compilerを導入している場合、多くの場面で手動の useMemo は不要になる。
ただし、サードパーティライブラリが参照の等価性を要求する場面等、細かい制御が必要なケースでは引き続き手動での useMemo が有効である。
なお、useMemo はパフォーマンス最適化のためのHookであり、計算結果の正確性を保証するものではない。
Reactは内部的にキャッシュを破棄する場合があるため、メモ化の保証を前提としたロジックは定義しないことを推奨する。
基本構文
useMemo のTypeScriptでのシグネチャを以下に示す。
// 基本的な使用方法
const memoizedValue = useMemo(calculateValue, dependencies);
// ジェネリック型を明示する場合
const memoizedValue = useMemo<型>(() => calculateValue(), dependencies);
下表に、パラメータの説明を示す。
| パラメータ | 説明 |
|---|---|
| calculateValue | キャッシュする値を計算する関数 引数を受け取らず、任意の型の値を返す。 純粋関数である必要があり、副作用を含めてはならない。 初回レンダリング時に実行され、依存配列の値が変化した時のみ再実行される。 |
| dependencies | 第1引数が参照する全てのリアクティブ値を含む配列 props、state、コンポーネント本体で宣言された変数・関数が対象である。 各要素は Object.is() で前回の値と比較される。
|
戻り値の型は、TypeScriptの型推論が自動的に適用される。
明示的にジェネリック型を指定する場合は useMemo<string>(...) のように記述する。
| パラメータ | 型 | 説明 |
|---|---|---|
| calculateValue | () => T |
キャッシュする値を計算する純粋関数 |
| dependencies | React.DependencyList |
calculateValueが参照するリアクティブ値の配列 |
| 戻り値 | T |
計算された値、または前回のキャッシュ値 |
使用例
基本的な使用例
数値のリストから合計・最大値・最小値を計算する重い処理をメモ化する例を以下に示す。
以下の例では、multiplier の状態が変化しても、numbers が変化しない限り統計値の計算は実行されない。
// Reactのhooksをインポート (useMemo: メモ化, useState: 状態管理)
import { useMemo, useState } from 'react';
// 統計データを表現するインターフェース (戻り値の型定義)
interface Statistics {
sum: number; // 合計値
max: number; // 最大値
min: number; // 最小値
average: number; // 平均値
}
// コンポーネントのprops型定義
interface StatsDashboardProps {
numbers: number[]; // 計算対象の数値配列
}
// 統計ダッシュボードコンポーネント
function StatsDashboard({ numbers }: StatsDashboardProps) {
// 乗算用の状態 (ボタンを押下すると変化する)
const [multiplier, setMultiplier] = useState(1);
// numbersが変化した場合のみ再計算される (メモ化された統計値)
const stats = useMemo<Statistics>(() => {
console.log('統計値を計算中...');
const sum = numbers.reduce((acc, n) => acc + n, 0); // 合計値を計算
const max = Math.max(...numbers); // 最大値を計算
const min = Math.min(...numbers); // 最小値を計算
const average = numbers.length > 0 ? sum / numbers.length : 0; // 平均値を計算 (0除算を防ぐためチェック付き)
// 計算結果をオブジェクトで返す
return { sum, max, min, average };
// 依存配列: numbersが変化した時のみ再計算
}, [numbers]);
// 画面描画 (JSXを返す)
return (
<div>
{/* 合計値に倍率を掛けて表示 */}
<p>合計: {stats.sum * multiplier}</p>
<p>最大値: {stats.max}</p>
<p>最小値: {stats.min}</p>
<p>平均値: {stats.average.toFixed(2)}</p>
{/* ボタンを押下するとmultiplierが増加 (統計値の再計算は発生しない) */}
<button onClick={() => setMultiplier(m => m + 1)}>
倍率を上げる (現在: {multiplier})
</button>
</div>
);
}
オブジェクト参照の安定化
オブジェクトや配列はレンダリングのたびに新しい参照が生成される。
useMemo でオブジェクトの参照を安定化させ、子コンポーネントへの不要な再レンダリングを防止することができる。
以下の例では、userConfig オブジェクトは theme が変化した時のみ新しい参照が生成されるため、ConfigDisplay の不要な再レンダリングを防止している。
// Reactのhooksとmemoをインポート
// useMemo : メモ化
// useState : 状態管理
// memo : コンポーネントのメモ化
import { useMemo, useState, memo } from 'react';
// ユーザ設定を保持するオブジェクトの型定義
interface UserConfig {
theme: string; // テーマ (light / dark等)
language: string; // 言語 (ja / en等)
fontSize: number; // フォントサイズ (px単位)
}
// ConfigDisplayコンポーネントのprops型定義
interface ConfigDisplayProps {
config: UserConfig; // ユーザ設定オブジェクト
}
// React.memoでラップされた子コンポーネント
// propsが変化しない限り再レンダリングされない
const ConfigDisplay = memo(({ config }: ConfigDisplayProps) => {
console.log('ConfigDisplayがレンダリングされた');
return (
<div>
<p>テーマ: {config.theme}</p>
<p>言語: {config.language}</p>
<p>フォントサイズ: {config.fontSize}px</p>
</div>
);
});
// ユーザ設定コンポーネント (親)
function UserSettings() {
const [theme, setTheme] = useState('light'); // テーマの状態管理
const [counter, setCounter] = useState(0); // カウンタの状態管理 (テーマとは無関係)
// useMemoなしの場合、counterが変化するたびに新しいオブジェクトが生成される
// ConfigDisplayはmemoでラップされていても毎回再レンダリングされてしまう
// useMemoありの場合、themeが変化した時のみ新しいオブジェクトが生成される
// 依存配列に[theme]を指定することにより、themeの変化のみをトリガーとする
const userConfig = useMemo<UserConfig>(() => ({
theme,
language: 'ja',
fontSize: 16,
}), [theme]);
// 画面描画
return (
<div>
{/* テーマを切り替えるボタン (押すとuserConfigが再生成される) */}
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
テーマを切り替える
</button>
{/* カウンタ増加ボタン (userConfigは再生成されない) */}
<button onClick={() => setCounter(c => c + 1)}>
カウンタ: {counter}
</button>
{/* 安定した参照を持つuserConfigを渡す */}
<ConfigDisplay config={userConfig} />
</div>
);
}
フィルタリングとソート
リストのフィルタリングやソート処理は、データが多い場合に計算コストが高くなる。
例えば、リアルタイム検索では入力のたびに再レンダリングが発生するため、フィルタリング処理のメモ化が特に効果的である。
以下の例では、検索クエリやソート条件が変化した時のみ再計算するようにメモ化している。
// Reactのhooksをインポート
// useMemo: メモ化, useState: 状態管理
import { useMemo, useState } from 'react';
// 商品データのインターフェース定義
interface Product {
id: number; // 商品ID
name: string; // 商品名
price: number; // 価格
category: string; // カテゴリ
}
// ソートキーの型定義
// name: 名前順, price: 価格順
type SortKey = 'name' | 'price';
// ProductListコンポーネントのprops型定義
interface ProductListProps {
products: Product[]; // 商品データの配列
}
// 商品リストコンポーネント
function ProductList({ products }: ProductListProps) {
const [query, setQuery] = useState(''); // 検索クエリの状態
const [sortKey, setSortKey] = useState<SortKey>('name'); // ソートキーの状態
const [selectedCategory, setSelectedCategory] = useState(''); // 選択カテゴリの状態
// フィルタリングとソートをメモ化する
// 依存配列のいずれかが変化した場合のみ再計算される
const filteredAndSorted = useMemo<Product[]>(() => {
// 元の配列をコピー (破壊的な操作を防ぐため)
let result = [...products];
// カテゴリでフィルタリング (カテゴリが選択されている場合)
if (selectedCategory) {
result = result.filter(p => p.category === selectedCategory);
}
// 検索クエリでフィルタリング (クエリが入力されている場合)
if (query) {
const lowerQuery = query.toLowerCase();
// 商品名がクエリを含むかをチェック (大文字小文字を無視)
result = result.filter(p => p.name.toLowerCase().includes(lowerQuery));
}
// ソート処理 (sortKeyに基づいてソート順を決定)
result.sort((a, b) => {
// 名前順ソートの場合
if (sortKey === 'name') return a.name.localeCompare(b.name);
// 価格順ソートの場合
return a.price - b.price;
});
// フィルタリングして、ソート済みの結果を返す
return result;
// 依存配列: これらの値が変化した場合のみ再計算
}, [products, query, sortKey, selectedCategory]);
// 画面描画
return (
<div>
{/* 検索入力フィールド */}
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="商品名で検索"
/>
{/* ソート順選択ドロップダウン */}
<select value={sortKey} onChange={e => setSortKey(e.target.value as SortKey)}>
<option value="name">名前順</option>
<option value="price">価格順</option>
</select>
{/* 検索結果件数の表示 */}
<p>{filteredAndSorted.length}件の商品</p>
{/* 商品リストの表示 */}
<ul>
{filteredAndSorted.map(p => (
// 各商品をリストアイテムとして表示 (keyにidを使用)
<li key={p.id}>{p.name} - {p.price}円</li>
))}
</ul>
</div>
);
}
JSX要素のメモ化
useMemo でJSX要素をメモ化するパターンを以下に示す。
JSX要素のメモ化は行数が多いテーブルや複雑なリストで有効である。
ただし、React Compilerが導入されている場合は自動的に最適化されるため、手動でのメモ化は不要になる場合が多い。
以下の例では、生成コストが高いリストや複雑なUIを依存値が変化した時のみ再生成指定る。
// Reactのhooksをインポート
// useMemo: メモ化, useState: 状態管理
import { useMemo, useState } from 'react';
// データ行のインターフェース定義
interface DataRow {
id: number; // 行ID
label: string; // ラベル
value: number; // 値
isHighlighted: boolean; // ハイライト表示フラグ
}
// DataTableコンポーネントのprops型定義
interface DataTableProps {
rows: DataRow[]; // データ行の配列
}
// データテーブルコンポーネント
function DataTable({ rows }: DataTableProps) {
const [filterHighlighted, setFilterHighlighted] = useState(false); // ハイライト行のみ表示するかどうかのフィルタフラグ
// JSX要素をメモ化する
// rowsまたはfilterHighlightedが変化した場合のみ再計算される
const tableRows = useMemo(() => {
// フィルタリング条件に応じて表示する行を決定
const displayRows = filterHighlighted
? rows.filter(r => r.isHighlighted) // ハイライト行のみ抽出
: rows; // 全行を表示
// 各行をJSX要素に変換
return displayRows.map(row => (
<tr
key={row.id}
// ハイライト行は背景色を変更 (薄い黄色)
style={{ background: row.isHighlighted ? '#fffbe6' : 'transparent' }}
>
<td>{row.id}</td>
<td>{row.label}</td>
{/* 値をロケールに応じた形式で表示 (3桁区切り等) */}
<td>{row.value.toLocaleString()}</td>
</tr>
));
// 依存配列: これらの値が変化した場合のみ再計算
}, [rows, filterHighlighted]);
// 画面描画
return (
<div>
{/* フィルタリングオプション */}
<label>
<input
type="checkbox"
checked={filterHighlighted}
onChange={e => setFilterHighlighted(e.target.checked)}
/>
ハイライト行のみ表示
</label>
{/* データテーブル */}
<table>
{/* テーブルヘッダー */}
<thead>
<tr>
<th>ID</th>
<th>ラベル</th>
<th>値</th>
</tr>
</thead>
{/* テーブルボディ (メモ化されたJSX要素を使用) */}
<tbody>{tableRows}</tbody>
</table>
</div>
);
}
React Compilerとの関係
React Compilerによる自動メモ化
React Compiler (正式名: React Forget) は2025年10月にv1.0がリリースされた、Reactの公式コンパイラである。
React Compilerは、コンポーネントのコードを静的解析して、useMemo、useCallback、React.memo に相当する最適化を自動的に適用する。
下表に、React Compilerを導入した場合の報告されているパフォーマンス改善を示す。
| 項目 | 改善内容 |
|---|---|
| 読み込み速度 | 最大12[%]向上 |
| 相互作用速度 | 最大2.5倍向上 |
| Sanity Studio | 全体で20〜30[%]のレンダリング時間削減 |
React Compilerを有効化する方法を以下に示す。
# babel-plugin-react-compilerをインストールする npm install babel-plugin-react-compiler
// babel.config.js または vite.config.ts での設定例
// 下記は、vite.config.tsの場合
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [['babel-plugin-react-compiler']],
},
}),
],
});
React Compilerが有効な場合、以下に示すような手動メモ化コードは自動的に同等の最適化が適用されるため、明示的な記述が不要になる。
// React Compiler導入前 : 手動でuseMemoを記述していた
const expensiveResult = useMemo(() => computeExpensive(data), [data]);
// React Compiler導入後 : コンパイラが自動的に最適化するため、useMemo無しで記述できる
const expensiveResult = computeExpensive(data);
新規に作成するコンポーネントでは、React Compilerに最適化を任せて useMemo を省略することを推奨する。
手動メモ化が必要なケース
React Compiler導入後も、以下のケースでは手動での useMemo が引き続き有効である。
| ケース | 説明 |
|---|---|
| サードパーティライブラリが参照の等価性を要求する場合 | 一部のライブラリ (例: React SelectのisOptionDisabled等) は、 渡されるオブジェクト・配列の参照が変化した場合に処理を再実行する。 React Compilerの自動最適化では対応できない場合があり、 手動での useMemo が必要になる。
|
| React Compilerが解析できない複雑なパターン | 外部ストア (Redux, Zustand等) との連携や、動的なHookの呼び出しパターン等が該当する。 |
| 既存コードの段階的な移行期間 | React Compilerを段階的に導入している場合、 Compiler未対応のコンポーネントでは従来通り手動メモ化が必要である。 |
useCallbackとの違い
useMemo と useCallback は、どちらもメモ化を行うHookであるが、メモ化する対象が異なる。
| 項目 | useMemo | useCallback |
|---|---|---|
| メモ化する対象 | 計算の結果 (値) | 関数の定義そのもの |
| 戻り値 | 計算された値 | メモ化された関数 |
| 主な用途 | 重い計算結果のキャッシュ | コールバック関数の参照安定化 |
useCallback(fn, deps) は、概念的に useMemo(() => fn, deps) と同等である。
以下のコードはいずれも同じ動作をする。
import { useMemo, useCallback } from 'react';
// useCallbackを使用する場合
const handleClick = useCallback((id: number) => {
console.log('クリックされた:', id);
}, []);
// useMemoで同等の動作を実現する場合
const handleClickWithMemo = useMemo(() => (id: number) => {
console.log('クリックされた:', id);
}, []);
関数をメモ化する場合は useCallback を使用した方が意図が明確になるため、関数のメモ化には useCallback を優先することを推奨する。
useMemo は計算結果の値をメモ化する場合に使用する。
// Reactのhooksとmemoをインポート
// useMemo: 値のメモ化, useCallback: 関数のメモ化, useState: 状態管理, memo: コンポーネントのメモ化
import { useMemo, useCallback, useState, memo } from 'react';
// 商品アイテムのインターフェース定義
interface Item {
id: number; // アイテムID
name: string; // アイテム名
price: number; // 価格 (税抜)
}
// ItemListコンポーネントのprops型定義
interface ItemListProps {
items: Item[]; // アイテム配列
taxRate: number; // 税率 (例: 0.1 = 10%)
}
// React.memoでラップされたアイテムリストコンポーネント
// propsが変化しない限り再レンダリングされない
const ItemList = memo(({ items, taxRate }: ItemListProps) => {
// 税込み価格を計算する (値をメモ化 : useMemoが適切)
// itemsまたはtaxRateが変化した場合のみ再計算される
const itemsWithTax = useMemo(() =>
// 各アイテムに税込み価格を追加した新しいオブジェクトを生成
items.map(item => ({
...item, // 既存のプロパティを展開
priceWithTax: Math.floor(item.price * (1 + taxRate)), // 税込み価格を計算 (小数点以下切り捨て)
})),
// 依存配列: itemsまたはtaxRateが変化した場合のみ再計算
[items, taxRate]);
// 削除ハンドラ (関数をメモ化 : useCallbackが適切)
// 依存配列が空のため、コンポーネントのライフサイクル中は同じ関数参照が維持される
const handleDelete = useCallback((id: number) => {
console.log('削除:', id);
}, []);
// 画面描画
return (
<ul>
{/* 各アイテムをリスト要素として表示 */}
{itemsWithTax.map(item => (
<li key={item.id}>
{/* アイテム名と税込み価格を表示 */}
{item.name} - {item.priceWithTax}円
{/* 削除ボタン (押下時にhandleDeleteを呼び出し) */}
<button onClick={() => handleDelete(item.id)}>削除</button>
</li>
))}
</ul>
);
});
注意事項
依存配列の不適切な指定
useMemo の依存配列に必要な値を省略した場合、古いキャッシュが返され続けるバグが発生する。
// 正しい : calculateValueが参照する全ての値を依存配列に含める
const filtered = useMemo(() => {
return items.filter(item => item.key === filterKey);
}, [items, filterKey]);
// 誤り : filterKeyを依存配列に含めていないため、filterKeyが変化しても再計算されない
const filtered = useMemo(() => {
return items.filter(item => item.key === filterKey);
}, [items]); // filterKeyが欠落している
ESLintの react-hooks/exhaustive-deps ルールを有効化すると、依存配列の欠落を自動検出できる。
また、依存配列に毎回新しい参照が生成されるオブジェクト・配列を含めると、依存値が常に変化したと判定され、メモ化の効果がなくなる。
function ParentComponent() {
// 正しい : オブジェクトをuseMemoの外でuseState / useMemoで安定化させる
const [limit] = useState(10);
const stableResult = useMemo(() => compute(data), [data, limit]);
// 誤り : レンダリングのたびに新しいoptions配列が生成される
const result = useMemo(() => compute(data), [data, { limit: 10 }]);
// ^^^^^^^^^^^^^^^^^
// 毎回新しいオブジェクト参照が生成される
}
メモ化の保証がない
Reactは特定の状況においてメモ化されたキャッシュを破棄する場合がある。
メモ化が常に機能することを前提としたロジックは定義しないことを推奨する。
キャッシュが破棄される可能性がある状況は以下の通りである。
- React Strict Mode
- 開発環境でのデバッグ目的でキャッシュが破棄される場合がある。
- Suspense
- コンポーネントのサスペンド・再開によってキャッシュが破棄される場合がある。
- Reactの将来のバージョン
- オフスクリーンコンポーネント等の機能でキャッシュ破棄が発生しうる。
// 正しい : 副作用なしの純粋な計算のみをuseMemoに含める
const processedData = useMemo(() => heavyCompute(data), [data]);
// 誤り : useMemoのキャッシュに副作用のある処理を依存させている
const processedData = useMemo(() => {
const result = heavyCompute(data);
// キャッシュが破棄・再計算された場合、このカウンタは複数回インクリメントされる
analyticsCounter++;
return result;
}, [data]);
副作用をuseMemo内で行わない
useMemo は純粋な計算のみを含める必要がある。
副作用 (APIリクエスト、DOMの直接操作、タイマの設定等) を useMemo 内で実行してはならない。
副作用が必要な場合は、useEffect を使用する。
// 正しい : 副作用はuseEffectで処理して、useMemoは純粋な計算のみにする
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
}, [userId]);
const userData = useMemo(() => computeDisplayData(localData), [localData]);
// 誤り : useMemo内でAPIリクエスト (副作用) を実行している
const userData = useMemo(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
return computeDisplayData(localData);
}, [userId, localData]);
useMemoを使うべきでない場面
以下に示す場面では、useMemo を使用しても効果がなく、むしろオーバーヘッドになる場合がある。
| 場面 | 説明 |
|---|---|
| 計算処理が軽い場合 | 単純な演算や短いリストの処理は、メモ化のコスト (依存配列の比較、キャッシュの保持) の方が高くなる場合がある。 |
| 依存配列の値が頻繁に変わる場合 | 毎レンダリングごとに依存値が変化するなら、メモ化の再利用が全く発生しない。 |
| コンポーネントが一度だけレンダリングされる場合 | キャッシュを再利用する機会がなければメモ化は無意味である。 |
| プリミティブ値をメモ化する場合 | 数値や文字列等のプリミティブ値は、同じ値であれば参照も同じであるため、メモ化は不要である。 |
パフォーマンスの最適化が必要かどうかは、React DevTools Profilerを使用して実測値に基づいて判断することを推奨する。
// 正しい : 軽い計算はメモ化せずに直接記述する
const doubled = count * 2;
const label = `ユーザ: ${name}`;
// 誤り : 軽い計算やプリミティブ値のメモ化 (不要なオーバーヘッド)
const doubled = useMemo(() => count * 2, [count]);
const label = useMemo(() => `ユーザ: ${name}`, [name]);
関連情報
- 公式ドキュメント - useMemo
- 公式ドキュメント - React Compiler
- Reactの基礎 - Hooksの基礎
- Reactの基礎 - useState
- Reactの基礎 - useEffect
- Reactの基礎 - useCallback
- Reactの基礎 - カスタムHook