React.memoの実務的な使い方|パフォーマンス最適化の実装パターン
React.memoとは|簡易的な解説
React.memoは、コンポーネントの不要な再レンダリングを防ぐための高階コンポーネント(HOC)です。propsが変わらなければ、前回のレンダリング結果を再利用します。
基本的な動作原理:
- propsの浅い比較(Shallow Comparison)を実行
- propsが同じなら再レンダリングをスキップ
- propsが異なれば通常通り再レンダリング
クラスコンポーネントのPureComponentと似た役割ですが、関数型コンポーネント向けの実装です。
業務でのユースケース|いつ使うべきか
実際の開発現場では、以下のような場面でReact.memoが活躍します:
ユースケース1:リスト表示でのパフォーマンス低下
大量のアイテムを表示するリストで、親コンポーネントの状態更新時に全アイテムが再レンダリングされる場合です。特にAPI呼び出しのローディング状態や、フィルター機能を持つ親コンポーネントで顕著です。
ユースケース2:複雑な計算を伴うコンポーネント
グラフやチャートの描画、データの複雑な加工処理を行うコンポーネントが、不要に再レンダリングされるケースです。
ユースケース3:外部ライブラリとの統合
Mapboxやd3.jsなどの外部ライブラリを使用するコンポーネントで、不要な初期化を避ける必要があります。
実装コード|実務で実際に使うパターン
パターン1:基本的な使い方(リストアイテム)
ECサイトの商品リスト表示を想定した実装です:
// ProductCard.tsx
import React from 'react';
interface ProductCardProps {
id: string;
title: string;
price: number;
onAddToCart: (id: string) => void;
imageUrl: string;
inStock: boolean;
}
// React.memoでコンポーネントをラップ
const ProductCard = React.memo(({
id,
title,
price,
onAddToCart,
imageUrl,
inStock,
}) => {
console.log(`Rendering ProductCard: ${id}`);
return (
{title}
¥{price.toLocaleString()}
{inStock ? '在庫あり' : '在庫なし'}
);
});
ProductCard.displayName = 'ProductCard';
export default ProductCard;
親コンポーネントの実装:
// ProductList.tsx
import React, { useState, useCallback } from 'react';
import ProductCard from './ProductCard';
interface Product {
id: string;
title: string;
price: number;
imageUrl: string;
inStock: boolean;
}
const ProductList: React.FC = () => {
const [products] = useState([
{
id: '1',
title: '商品A',
price: 5000,
imageUrl: '/product-a.jpg',
inStock: true,
},
{
id: '2',
title: '商品B',
price: 8000,
imageUrl: '/product-b.jpg',
inStock: false,
},
{
id: '3',
title: '商品C',
price: 3000,
imageUrl: '/product-c.jpg',
inStock: true,
},
]);
const [cart, setCart] = useState([]);
const [sortBy, setSortBy] = useState<'price' | 'name'>('price');
// 重要:useCallbackでコールバック関数を安定化させる
// これがないと、ProductCardは常に異なるpropsを受け取り、memoが効かない
const handleAddToCart = useCallback((productId: string) => {
setCart((prev) => [...prev, productId]);
}, []);
return (
カート内:{cart.length}件
{products.map((product) => (
))}
);
};
export default ProductList;
パターン2:カスタム比較関数の使用
デフォルトの浅い比較では不十分な場合、カスタム比較関数を使用できます。ユーザー情報の表示で、同じユーザーIDなら詳細情報が異なっても再レンダリングしないようにするケースです:
// UserProfile.tsx
import React from 'react';
interface UserData {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
interface UserProfileProps {
user: UserData;
isHighlighted: boolean;
}
const UserProfile = React.memo(
({ user, isHighlighted }) => {
console.log(`Rendering UserProfile: ${user.id}`);
return (
{user.name}
{user.email}
テーマ:{user.preferences.theme}
通知:{user.preferences.notifications ? 'ON' : 'OFF'}
);
},
// カスタム比較関数:ユーザーIDと名前だけで比較
(prevProps, nextProps) => {
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.isHighlighted === nextProps.isHighlighted
);
}
);
UserProfile.displayName = 'UserProfile';
export default UserProfile;
パターン3:複雑なコンポーネント構造での実装
ダッシュボード画面で複数のウィジェットがある場合の実装例です:
// Dashboard.tsx
import React, { useState, useCallback } from 'react';
interface MetricsWidgetProps {
title: string;
value: number;
unit: string;
trend: number;
onRefresh: () => void;
}
// メトリクス表示ウィジェット
const MetricsWidget = React.memo(({
title,
value,
unit,
trend,
onRefresh,
}) => {
console.log(`Rendering MetricsWidget: ${title}`);
const trendColor = trend >= 0 ? 'green' : 'red';
const trendSymbol = trend >= 0 ? '↑' : '↓';
return (
{title}
{value}{unit}
{trendSymbol} {Math.abs(trend)}%
);
});
MetricsWidget.displayName = 'MetricsWidget';
const Dashboard: React.FC = () => {
const [metrics, setMetrics] = useState({
revenue: { value: 1500000, trend: 5 },
users: { value: 3450, trend: -2 },
engagement: { value: 78, trend: 12 },
});
const [selectedTimeRange, setSelectedTimeRange] = useState('week');
// 各メトリクスのリフレッシュ関数をuseCallbackで安定化
const handleRefreshRevenue = useCallback(() => {
setMetrics((prev) => ({
...prev,
revenue: { ...prev.revenue, value: Math.floor(Math.random() * 2000000) },
}));
}, []);
const handleRefreshUsers = useCallback(() => {
setMetrics((prev) => ({
...prev,
users: { ...prev.users, value: Math.floor(Math.random() * 5000) },
}));
}, []);
const handleRefreshEngagement = useCallback(() => {
setMetrics((prev) => ({
...prev,
engagement: { ...prev.engagement, value: Math.floor(Math.random() * 100) },
}));
}, []);
return (
);
};
export default Dashboard;
よくある応用パターン
パターン1:useMemoとの組み合わせ
computedな値をpropsとして渡す場合、useMemoで計算結果を安定化させます:
// ParentComponent.tsx
import React, { useMemo, useCallback } from 'react';
import ChildComponent from './ChildComponent';
interface ParentProps {
items: { id: string; name: string; price: number }[];
}
const ParentComponent: React.FC = ({ items }) => {
// 複雑な計算結果を安定化させる
const totalPrice = useMemo(
() => items.reduce((sum, item) => sum + item.price, 0),
[items]
);
const discountedPrice = useMemo(
() => Math.floor(totalPrice * 0.9),
[totalPrice]
);
const handleCheckout = useCallback(() => {
console.log(`Checkout: ${discountedPrice}円`);
}, [discountedPrice]);
return (
合計:{discountedPrice}円
);
};
export default ParentComponent;
パターン2:コンテキストと組み合わせたグローバル状態の管理
Contextを使う場合でも、値の安定化が重要です:
// ThemeContext.tsx
import React, { createContext, useState, useMemo, useCallback } from 'react';
export type ThemeType = 'light' | 'dark';
interface ThemeContextValue {
theme: ThemeType;
toggleTheme: () => void;
}
export const ThemeContext = createContext(undefined);
interface ThemeProviderProps {
children: React.ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
}, []);
// valueオブジェクト自体を安定化させる
const value = useMemo(
() => ({
theme,
toggleTheme,
}),
[theme, toggleTheme]
);
return (
{children}
);
};
// useThemeフック
export const useTheme = (): ThemeContextValue => {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// 子コンポーネント
const ThemedButton = React.memo(() => {
const { theme, toggleTheme } = useTheme();
console.log('Rendering ThemedButton');
return (
);
});
ThemedButton.displayName = 'ThemedButton';
注意点と落とし穴
注意点1:useCallbackを忘れると効果がない
React.memoを使う際、最も一般的な間違いはコールバック関数をuseCallbackでラップしないことです:
// ❌ 悪い例:毎回新しい関数が作られる
const BadParent: React.FC = () => {
return (
console.log('clicked')}
/>
);
};
// ✅ 良い例:関数を安定化させる
const GoodParent: React.FC = () => {
const handlePress = useCallback(() => {
console.log('clicked');
}, []);
return ;
};
注意点2:オブジェクト型のpropsの扱い
propsがオブジェクト型の場合、毎回新しいオブジェクトが生成されるとmemoは効きません:
// ❌ 悪い例:毎回新しいオブジェクトが作られる
const BadExample: React.FC = () => {
return (
);
};
// ✅ 良い例:stateまたはuseMemoで安定化
const GoodExample: React.FC = () => {
const user = useMemo(() => ({ id: '1', name: 'John' }), []);
return ;
};
注意点3:過度な最適化は避ける
すべてのコンポーネントをReact.memoでラップする必要はありません。パフォーマンスプロファイラーで実際に問題を確認してから使用しましょう:
// React DevToolsのProfilerで測定
import { Profiler } from 'react';
const App: React.FC = () => {
const handleRenderCallback = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
interactions: Set
) => {
console.log(`${id} (${phase}): ${actualDuration}ms`);
};
return (
);
};
注意点4:依存配列の管理が重要
useCallbackの依存配列を正しく指定しないと、予期しない再レンダリングが発生します:
// ❌ 悪い例:依存配列が空で、常に古い値を参照
const BadDependency: React.FC<{ items: Item[] }> = ({ items }) => {
const handleSubmit = useCallback(() => {
console.log(items); // 常に最初のitemsを参照
}, []);
return ;
};
// ✅ 良い例:依存配列に含める
const GoodDependency: React.FC<{ items: Item[] }> = ({ items }) => {
const handleSubmit = useCallback(() => {
console.log(items); // 常に最新のitemsを参照
}, [items]);
return ;
};
パフォーマンス測定のコツ
実務では、実際に効果があるか測定することが重要です:
// PerformanceTest.tsx
import React, { useState, useCallback } from 'react';
import { Profiler, ProfilerOnRenderCallback } from 'react';
interface RenderMetrics {
componentId: string;
duration: number;
timestamp: number;
isMemoized: boolean;
}
const PerformanceTest: React.FC = () => {
const [metrics, setMetrics] = useState([]);
const onRenderCallback: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
setMetrics((prev) => [
...prev,
{
componentId: id,
duration: actualDuration,
timestamp: Date.now(),
isMemoized: actualDuration < 1,
},
].slice(-20)); // 最新20件のみ保持
};
const average =
metrics.length > 0
? (metrics.reduce((sum, m) => sum + m.duration, 0) / metrics.length).toFixed(2)
: 0;
return (
パフォーマンス測定
平均レンダリング時間:{average}ms
{/* テスト対象のコンポーネント */}
{metrics.map((m, i) => (
{m.componentId}: {m.duration.toFixed(3)}ms
{m.isMemoized && ' ✓ Optimized'}
))}
);
};
export default PerformanceTest;
実務でのベストプラクティス
1. チェックリストで判断する
- コンポーネントはpropsの数が3個以上か?
- 親コンポーネントが頻繁に再レンダリングされるか?
- 計算や副作用を含むか?
- 上記すべてに「はい」なら使用を検討する
2. displayNameを設定する
React DevToolsで識別しやすくするため、必ずdisplayNameを設定します:
const MyComponent = React.memo(() => Content);
MyComponent.displayName = 'MyComponent';
3. カスタム比較関数は最後の手段
デフォルトの浅い比較で十分な場合がほとんどです。カスタム比較関数はテストが複雑になるため、本当に必要な場合だけ使用します。
4. ESLintプラグインを活用する
eslint-plugin-reactの設定で、useCallbackの欠落を自動検出できます:
// .eslintrc.json
{
\"plugins\": [\"react\"],
\"rules\": {
\"react/display-name\": \"warn\",
\"react-hooks/exhaustive-deps\": \"warn\",
\"react-hooks/rules-of-hooks\": \"error\"
}
}
まとめ
React.memoは実務でのパフォーマンス最適化に欠かせないツールです。基本的なポイントをまとめます:
- 基本: propsが変わらなければ再レンダリングをスキップ
- 組み合わせ: useCallbackとuseMemoを必ず組み合わせる
- 測定: プロファイラーで実際に効果を確認する
- 判断: すべてのコンポーネントに使う必要はない
- 保守性: displayNameを設定し、ESLintを活用する
実務では、リスト表示やダッシュボードのウィジェット表示など、親コンポーネントの更新が頻繁に発生する場合にReact.memoの効果が大きいです。ただし、無理に最適化するのではなく、実際のパフォーマンス測定に基づいて判断することが成功の鍵です。
適切に使用すれば、ユーザー体験の大幅な改善につながり、保守性の高いコードベースを構築できます。

