React useMemoの業務パターン解説|実務で使える最適化テクニック

React / Next.js

React useMemoの業務パターン解説|実務で使える最適化テクニック

Reactアプリケーションを開発していると、パフォーマンスの問題に直面することがあります。その際に活躍するのがuseMemoフックです。ただし、むやみに使うと逆効果になることもあります。本記事では、実務で実際に使われるuseMemoのパターンを、具体的なコード例とともに解説していきます。

useMemoの簡易的な解説

useMemoは、計算量が多い処理の結果をメモ化し、依存配列の値が変わるまでその結果をキャッシュするReactフックです。不必要な再計算を防ぎ、コンポーネントのパフォーマンスを向上させます。

基本的な構文は以下の通りです:

const memoizedValue = useMemo(() => {
  return expensiveCalculation();
}, [dependency1, dependency2]);

第一引数に計算処理を含むコールバック関数、第二引数に依存配列を指定します。依存配列の値が変わらない限り、メモ化された値が返されます。

業務でのユースケース

実務ではどのような場面でuseMemoが活躍するのでしょうか。主なケースを紹介します。

ケース1:大量データのフィルタリングと並べ替え

ECサイトやデータ管理ツールでは、数千〜数万件のデータをフィルタリングや並べ替えする場面が頻繁にあります。毎回レンダリング時に処理を実行していると、ユーザー入力の遅延につながります。

ケース2:複雑な計算結果の子コンポーネントへの配下

親コンポーネントで計算した複雑なオブジェクトを子コンポーネントに渡す場合、毎回新しいオブジェクトが生成されると、子コンポーネントの不必要な再レンダリングが発生します。

ケース3:グラフやチャートのデータ生成

ダッシュボード画面などで、複数のデータソースから集計データを生成し、グラフやチャートで表示する場合、頻繁な再計算は避けたいです。

実装コード

実務コード例1:フィルタリング・ソート機能

実際のプロダクション環境で使われるコード例です。ユーザー管理画面を想定しています。

import React, { useMemo, useState, useCallback } from 'react';\n\ninterface User {\n  id: number;\n  name: string;\n  email: string;\n  department: string;\n  salary: number;\n  joinDate: string;\n}\n\ninterface FilterParams {\n  searchText: string;\n  department: string | null;\n  sortBy: 'name' | 'salary' | 'joinDate';\n  sortOrder: 'asc' | 'desc';\n}\n\nconst UserListComponent: React.FC<{ users: User[] }> = ({ users }) => {\n  const [filters, setFilters] = useState({\n    searchText: '',\n    department: null,\n    sortBy: 'name',\n    sortOrder: 'asc',\n  });\n\n  // useMemoで計算結果をメモ化\n  const filteredAndSortedUsers = useMemo(() => {\n    console.log('フィルタリング・ソート実行');\n    \n    let result = users.filter(user => {\n      const matchesSearch = user.name.toLowerCase().includes(\n        filters.searchText.toLowerCase()\n      ) || user.email.toLowerCase().includes(\n        filters.searchText.toLowerCase()\n      );\n      \n      const matchesDept = !filters.department || \n        user.department === filters.department;\n      \n      return matchesSearch && matchesDept;\n    });\n\n    // ソート処理\n    result.sort((a, b) => {\n      let compareValue = 0;\n      \n      switch (filters.sortBy) {\n        case 'name':\n          compareValue = a.name.localeCompare(b.name);\n          break;\n        case 'salary':\n          compareValue = a.salary - b.salary;\n          break;\n        case 'joinDate':\n          compareValue = new Date(a.joinDate).getTime() - \n            new Date(b.joinDate).getTime();\n          break;\n      }\n      \n      return filters.sortOrder === 'asc' ? compareValue : -compareValue;\n    });\n\n    return result;\n  }, [\n    users,\n    filters.searchText,\n    filters.department,\n    filters.sortBy,\n    filters.sortOrder,\n  ]);\n\n  const handleSearchChange = useCallback((text: string) => {\n    setFilters(prev => ({ ...prev, searchText: text }));\n  }, []);\n\n  const handleDepartmentChange = useCallback((dept: string | null) => {\n    setFilters(prev => ({ ...prev, department: dept }));\n  }, []);\n\n  return (\n    
\n
\n handleSearchChange(e.target.value)}\n />\n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n {filteredAndSortedUsers.map(user => (\n \n \n \n \n \n \n \n ))}\n \n
名前メール部門給与入社日
{user.name}{user.email}{user.department}¥{user.salary.toLocaleString()}{user.joinDate}
\n

表示件数:{filteredAndSortedUsers.length}

\n
\n );\n};\n\nexport default UserListComponent;

実務コード例2:複雑なオブジェクト生成と配下

以下は、複数のAPIレスポンスから統計データを生成し、複数の子コンポーネントに渡すパターンです。

import React, { useMemo } from 'react';\n\ninterface SalesData {\n  productId: string;\n  quantity: number;\n  price: number;\n  date: string;\n}\n\ninterface DailyStats {\n  date: string;\n  totalRevenue: number;\n  totalQuantity: number;\n  averageOrderValue: number;\n  transactionCount: number;\n}\n\ninterface Statistics {\n  dailyStats: DailyStats[];\n  topProducts: Array<{ productId: string; revenue: number }>;\n  monthlyGrowthRate: number;\n}\n\nconst SalesDashboard: React.FC<{ salesData: SalesData[] }> = ({\n  salesData,\n}) => {\n  const stats = useMemo(() => {\n    console.log('統計計算実行');\n\n    // 日次統計の計算\n    const dailyMap = new Map();\n\n    salesData.forEach(sale => {\n      const existing = dailyMap.get(sale.date) || {\n        date: sale.date,\n        totalRevenue: 0,\n        totalQuantity: 0,\n        transactionCount: 0,\n        averageOrderValue: 0,\n      };\n\n      existing.totalRevenue += sale.quantity * sale.price;\n      existing.totalQuantity += sale.quantity;\n      existing.transactionCount += 1;\n      existing.averageOrderValue = existing.totalRevenue / existing.transactionCount;\n\n      dailyMap.set(sale.date, existing);\n    });\n\n    const dailyStats = Array.from(dailyMap.values()).sort(\n      (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()\n    );\n\n    // トップ商品の抽出\n    const productMap = new Map();\n    salesData.forEach(sale => {\n      const current = productMap.get(sale.productId) || 0;\n      productMap.set(sale.productId, current + sale.quantity * sale.price);\n    });\n\n    const topProducts = Array.from(productMap.entries())\n      .map(([productId, revenue]) => ({ productId, revenue }))\n      .sort((a, b) => b.revenue - a.revenue)\n      .slice(0, 5);\n\n    // 月間成長率の計算\n    let monthlyGrowthRate = 0;\n    if (dailyStats.length >= 30) {\n      const firstHalf = dailyStats\n        .slice(0, 15)\n        .reduce((sum, day) => sum + day.totalRevenue, 0);\n      const secondHalf = dailyStats\n        .slice(-15)\n        .reduce((sum, day) => sum + day.totalRevenue, 0);\n      monthlyGrowthRate = ((secondHalf - firstHalf) / firstHalf) * 100;\n    }\n\n    return {\n      dailyStats,\n      topProducts,\n      monthlyGrowthRate,\n    };\n  }, [salesData]);\n\n  return (\n    
\n

販売ダッシュボード

\n
\n \n \n \n
\n
\n );\n};\n\nconst ChartComponent: React.FC<{ dailyStats: DailyStats[] }> = ({\n dailyStats,\n}) => {\n console.log('ChartComponent レンダリング');\n return
チャート表示: {dailyStats.length}日分のデータ
;\n};\n\nconst TopProductsComponent: React.FC<{\n topProducts: Array<{ productId: string; revenue: number }>;\n}> = ({ topProducts }) => {\n console.log('TopProductsComponent レンダリング');\n return (\n
\n トップ商品:\n {topProducts.map(p => (\n
\n {p.productId}: ¥{p.revenue.toLocaleString()}\n
\n ))}\n
\n );\n};\n\nconst GrowthRateComponent: React.FC<{ growthRate: number }> = ({\n growthRate,\n}) => {\n console.log('GrowthRateComponent レンダリング');\n return
月間成長率: {growthRate.toFixed(2)}%
;\n};\n\nexport default SalesDashboard;

実務コード例3:キャッシュ戦略を組み込んだパターン

大規模なデータセットを扱う場合、独自のキャッシュ戦略を組み込むことがあります。

import React, { useMemo, useState, useRef } from 'react';\n\ninterface CacheEntry {\n  data: T;\n  timestamp: number;\n}\n\ninterface Product {\n  id: string;\n  name: string;\n  price: number;\n  category: string;\n  stock: number;\n}\n\nconst useAdvancedMemo = (\n  factory: () => T,\n  deps: React.DependencyList,\n  cacheTimeMs: number = 5000\n) => {\n  const cacheRef = useRef | null>(null);\n\n  return useMemo(() => {\n    const now = Date.now();\n    \n    // キャッシュが存在し、有効期限内の場合はそれを返す\n    if (\n      cacheRef.current &&\n      now - cacheRef.current.timestamp < cacheTimeMs\n    ) {\n      console.log('キャッシュから返却');\n      return cacheRef.current.data;\n    }\n\n    console.log('新規計算実行');\n    const result = factory();\n    cacheRef.current = { data: result, timestamp: now };\n    return result;\n  }, deps);\n};\n\nconst ProductSearchComponent: React.FC<{ products: Product[] }> = ({\n  products,\n}) => {\n  const [searchQuery, setSearchQuery] = useState('');\n  const [selectedCategory, setSelectedCategory] = useState(null);\n\n  const searchResults = useAdvancedMemo(\n    () => {\n      console.log('検索処理実行');\n      return products.filter(product => {\n        const matchesQuery = product.name\n          .toLowerCase()\n          .includes(searchQuery.toLowerCase());\n        const matchesCategory = !selectedCategory || \n          product.category === selectedCategory;\n        return matchesQuery && matchesCategory && product.stock > 0;\n      });\n    },\n    [products, searchQuery, selectedCategory],\n    3000 // 3秒間キャッシュ\n  );\n\n  return (\n    
\n setSearchQuery(e.target.value)}\n />\n \n
\n {searchResults.map(product => (\n
\n

{product.name}

\n

¥{product.price.toLocaleString()}

\n

在庫: {product.stock}

\n
\n ))}\n
\n
\n );\n};\n\nexport default ProductSearchComponent;

よくある応用パターン

パターン1:useMemo + useCallback の組み合わせ

子コンポーネントへの関数配下と組み合わせることで、効果的な最適化が実現できます。

import React, { useMemo, useCallback, memo } from 'react';\n\ninterface Item {\n  id: string;\n  name: string;\n  value: number;\n}\n\nconst ItemListManager: React.FC<{ items: Item[] }> = ({ items }) => {\n  const [selectedIds, setSelectedIds] = React.useState>(\n    new Set()\n  );\n\n  const selectedItems = useMemo(() => {\n    return items.filter(item => selectedIds.has(item.id));\n  }, [items, selectedIds]);\n\n  const handleSelect = useCallback((id: string) => {\n    setSelectedIds(prev => {\n      const newSet = new Set(prev);\n      if (newSet.has(id)) {\n        newSet.delete(id);\n      } else {\n        newSet.add(id);\n      }\n      return newSet;\n    });\n  }, []);\n\n  return (\n    
\n \n \n
\n );\n};\n\nconst ItemList = memo(\n ({\n items,\n onSelect,\n }: {\n items: Item[];\n onSelect: (id: string) => void;\n }) => {\n console.log('ItemList レンダリング');\n return (\n
\n {items.map(item => (\n \n ))}\n
\n );\n }\n);\n\nconst SelectedItemsSummary = memo(\n ({ items }: { items: Item[] }) => {\n console.log('SelectedItemsSummary レンダリング');\n const total = useMemo(\n () => items.reduce((sum, item) => sum + item.value, 0),\n [items]\n );\n\n return
合計: {total}
;\n }\n);\n\nexport default ItemListManager;

パターン2:条件付きメモ化

特定の条件下でのみメモ化を活用するパターンです。

import React, { useMemo } from 'react';\n\ninterface DataPoint {\n  timestamp: number;\n  value: number;\n}\n\nconst ConditionalMemoComponent: React.FC<{\n  dataPoints: DataPoint[];\n  enableOptimization: boolean;\n}> = ({ dataPoints, enableOptimization }) => {\n  const processedData = useMemo(() => {\n    // 大規模なデータセット (1000件以上) の場合のみメモ化\n    if (!enableOptimization || dataPoints.length < 1000) {\n      return dataPoints.map(point => ({\n        ...point,\n        hour: new Date(point.timestamp).getHours(),\n      }));\n    }\n\n    console.log('大規模データセット用の最適化を適用');\n    return dataPoints\n      .reduce((acc, point) => {\n        const hour = new Date(point.timestamp).getHours();\n        const existing = acc.find(p => p.hour === hour);\n        if (existing) {\n          existing.value = (existing.value + point.value) / 2;\n        } else {\n          acc.push({ ...point, hour });\n        }\n        return acc;\n      }, [] as any[])\n      .sort((a, b) => a.hour - b.hour);\n  }, [dataPoints, enableOptimization]);\n\n  return (\n    
\n

処理後のデータ件数: {processedData.length}

\n
\n );\n};\n\nexport default ConditionalMemoComponent;

注意点と落とし穴

注意点1:過度なメモ化は避ける

useMemoは銀の弾ではありません。すべての計算をメモ化すると、かえってパフォーマンスが低下することがあります。React開発者ツールで実際の実行時間を測定し、必要な箇所にのみ使用してください。

// 悪い例:シンプルな計算をメモ化
const result = useMemo(() => a + b, [a, b]); // オーバーヘッドが大きい\n\n// 良い例:複雑な計算をメモ化\nconst result = useMemo(() => {\n  // 複数の操作が含まれる\n  return expensiveArray.filter(...).map(...).reduce(...);\n}, [expensiveArray]);

注意点2:依存配列の不備

依存配列に必要な値を漏らすと、意図しない古いデータが返されます。ESLintのプラグイン(eslint-plugin-react-hooks)を導入して、警告を自動検出しましょう。

// 悪い例:依存配列が空(これは避けるべき)\nconst result = useMemo(() => {\n  return data.filter(item => item.category === category);\n}, []); // categoryの更新が反映されない!\n\n// 良い例:すべての依存値を含める\nconst result = useMemo(() => {\n  return data.filter(item => item.category === category);\n}, [data, category]);

注意点3:オブジェクトの参照比較

依存配列にオブジェクトを含める場合、毎回新しいオブジェクトが生成されるとメモ化の効果がなくなります。

// 悪い例\nconst filterObj = { category: 'electronics', inStock: true };\nconst filtered = useMemo(() => {\n  return products.filter(p => \n    p.category === filterObj.category && p.inStock === filterObj.inStock\n  );\n}, [products, filterObj]); // 毎回フィルター実行される\n\n// 良い例\nconst { category, inStock } = filters;\nconst filtered = useMemo(() => {\n  return products.filter(p => \n    p.category === category && p.inStock === inStock\n  );\n}, [products, category, inStock]);

注意点4:メモリ使用量の増加

メモ化されたデータはメモリに残ります。特に大規模なデータセットを複数メモ化する場合は注意が必要です。必要に応じてWeakMapやLRUキャッシュの使用も検討してください。

実務での推奨事項

以下のチェックリストを使用して、useMemoの使用判断をしてください。

  • 計算処理が明らかに重い(配列の大規模なフィルタリング、複雑な変換など)
  • 依存値の変更頻度が低い
  • メモ化で実際のパフォーマンス向上が測定できる
  • 子コンポーネントがReact.memoで最適化されている
  • ESLintで警告が出ていない

まとめ

React のuseMemoは、正しく使用すれば実務でのパフォーマンス最適化に大きな効果があります。本記事で紹介したように、大規模データのフィルタリング、複雑な計算の結果キャッシング、統計データの生成などが典型的なユースケースです。

ただし重要なのは、すべての計算をメモ化することではなく、実際のボトルネックを測定し、必要な箇所に戦略的に適用することです。依存配列の正確性、オブジェクト参照の管理、メモリ使用量への配慮を忘れずに、堅牢で高速なReactアプリケーションを構築しましょう。

プロダクション環境では、React DevToolsやChrome DevToolsを活用してパフォーマンス計測を習慣化することを強くお勧めします。

タイトルとURLをコピーしました