React useMemoの使い方|計算結果を効率的にキャッシュする

React useMemoフックを使って計算結果をキャッシュし、パフォーマンスを向上させる方法を解説。基本的な使い方から実践的な応用例まで、コード例とともに詳しく説明します。

Learning Next 運営
28 分で読めます

Reactアプリをサクサクにしよう!useMemoで計算を効率化する方法

みなさん、Reactアプリケーションで「処理が重い」と感じたことはありませんか?

「リストの計算が毎回実行されて遅い」 「コンポーネントが再レンダリングするたびに重い処理が走る」 「パフォーマンスを改善する方法がわからない」

そんな悩み、ありますよね。

実は、そんな問題を解決してくれる便利な機能があるんです。 それがReact useMemoフックです!

この記事では、useMemoを使って計算結果を効率的にキャッシュし、アプリケーションのパフォーマンスを向上させる方法をわかりやすく解説します。

基本的な使い方から実践的な応用例まで、実際のコード例とともに学んでいきましょう。

useMemoって何?基本から理解してみよう

useMemoの正体を知る

useMemoは計算結果をメモ化(キャッシュ)するReactフックです。

簡単に言うと、「前回と同じ条件なら、前回の計算結果を再利用しよう」という機能なんです。

依存関係が変わらない限り、前回の計算結果を再利用してパフォーマンスを向上させます。

基本的な使い方を見てみよう

まずは、useMemoの基本構文から見てみましょう。

import { useMemo } from 'react';

function MyComponent({ items }) {
  const expensiveValue = useMemo(() => {
    // 重い計算処理
    return items.filter(item => item.active).length;
  }, [items]); // 依存配列

  return <div>アクティブなアイテム数: {expensiveValue}</div>;
}

基本的な構文はとてもシンプルです。

第一引数:計算を行う関数 第二引数:依存配列(この値が変わったときだけ再計算される)

たったこれだけで、パフォーマンスの改善ができるんです!

動作原理を詳しく見てみよう

useMemoがどのように動作するか、比較してみましょう。

function ExpensiveCalculation({ numbers }) {
  // useMemoを使わない場合:毎回計算される
  const sum = numbers.reduce((acc, num) => acc + num, 0);
  
  // useMemoを使う場合:numbersが変わったときだけ計算される
  const memoizedSum = useMemo(() => {
    console.log('計算実行!'); // デバッグ用
    return numbers.reduce((acc, num) => acc + num, 0);
  }, [numbers]);
  
  return (
    <div>
      <p>通常の計算: {sum}</p>
      <p>メモ化された計算: {memoizedSum}</p>
    </div>
  );
}

この例を実行してみてください。

コンソールを見ると、「計算実行!」が表示されるのはnumbersが変わったときだけです。 他の原因で再レンダリングが発生しても、計算は実行されません。

これがuseMemoの威力なんです!

useMemoを使うべき場面と避けるべき場面

使うべき場面を知ろう

useMemoは適切な場面で使うことが重要です。 無駄に使うとかえってパフォーマンスが悪くなることもあります。

以下のような場面でuseMemoを使うと効果的です。

重い計算処理

商品データの複雑な集計処理を見てみましょう。

function ProductList({ products }) {
  // 商品データの複雑な集計処理
  const statistics = useMemo(() => {
    return {
      totalPrice: products.reduce((sum, p) => sum + p.price, 0),
      averageRating: products.reduce((sum, p) => sum + p.rating, 0) / products.length,
      categoryCount: products.reduce((acc, p) => {
        acc[p.category] = (acc[p.category] || 0) + 1;
        return acc;
      }, {})
    };
  }, [products]);
  
  return (
    <div>
      <h2>商品統計</h2>
      <p>総額: ¥{statistics.totalPrice}</p>
      <p>平均評価: {statistics.averageRating.toFixed(2)}</p>
      <p>カテゴリ別商品数: {JSON.stringify(statistics.categoryCount)}</p>
    </div>
  );
}

このコードでは、商品データから統計情報を計算しています。

商品データ(products)が変わらない限り、統計計算は再実行されません。 他の状態が変わって再レンダリングが発生しても、計算結果は前回のものを使い回します。

配列のフィルタリング・ソート

ユーザーリストの検索とソート機能を見てみましょう。

function UserList({ users, searchTerm, sortBy }) {
  // フィルタリングとソートの処理をメモ化
  const filteredAndSortedUsers = useMemo(() => {
    return users
      .filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        if (sortBy === 'age') return a.age - b.age;
        return 0;
      });
  }, [users, searchTerm, sortBy]);
  
  return (
    <ul>
      {filteredAndSortedUsers.map(user => (
        <li key={user.id}>{user.name} ({user.age}歳)</li>
      ))}
    </ul>
  );
}

この例では、検索とソートの処理をメモ化しています。

userssearchTermsortByのどれかが変わったときだけ処理が実行されます。 他の状態変更では、前回の結果をそのまま使用するんです。

複雑なオブジェクトの作成

チャートコンポーネントの設定オブジェクト作成を見てみましょう。

function ChartComponent({ data, options }) {
  // チャートの設定オブジェクトをメモ化
  const chartConfig = useMemo(() => {
    return {
      type: 'line',
      data: {
        labels: data.map(d => d.date),
        datasets: [{
          label: '売上',
          data: data.map(d => d.revenue),
          borderColor: options.color || '#007bff'
        }]
      },
      options: {
        responsive: true,
        scales: {
          y: {
            beginAtZero: true,
            max: Math.max(...data.map(d => d.revenue)) * 1.1
          }
        }
      }
    };
  }, [data, options]);
  
  return <Chart config={chartConfig} />;
}

チャートの設定オブジェクトは作成に時間がかかることがあります。 データやオプションが変わったときだけ再作成することで、パフォーマンスを向上できます。

使わない方が良い場面

以下のような場合はuseMemoを使わない方が良いでしょう。

軽い計算処理

// ❌ 軽い計算にuseMemoは不要
const doubledValue = useMemo(() => value * 2, [value]);

// ✅ そのまま計算した方が速い
const doubledValue = value * 2;

簡単な計算にuseMemoを使うと、かえってオーバーヘッドが発生します。

依存関係が毎回変わる場合

// ❌ 毎回新しいオブジェクトが作られるため意味がない
const result = useMemo(() => {
  return expensiveCalculation(data);
}, [{ ...data }]); // 毎回新しいオブジェクト

// ✅ 適切な依存関係
const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data.id, data.status]); // 必要な値のみ

依存関係が毎回変わる場合、メモ化の効果がありません。 必要な値だけを依存配列に入れるようにしましょう。

実践的な使用例を見てみよう

検索機能付きリストコンポーネント

実際の開発でよく使われるパターンを見てみましょう。

import { useState, useMemo } from 'react';

function SearchableList({ items }) {
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');
  
  // 検索とフィルタリングの結果をメモ化
  const filteredItems = useMemo(() => {
    return items.filter(item => {
      const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesSearch && matchesCategory;
    });
  }, [items, searchTerm, category]);
  
  // カテゴリ一覧の生成もメモ化
  const categories = useMemo(() => {
    const uniqueCategories = [...new Set(items.map(item => item.category))];
    return ['all', ...uniqueCategories];
  }, [items]);
  
  return (
    <div>
      <input
        type="text"
        placeholder="商品名で検索"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <select value={category} onChange={(e) => setCategory(e.target.value)}>
        {categories.map(cat => (
          <option key={cat} value={cat}>
            {cat === 'all' ? '全てのカテゴリ' : cat}
          </option>
        ))}
      </select>
      
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>
            {item.name} - {item.category}
          </li>
        ))}
      </ul>
    </div>
  );
}

この例のポイントを説明しますね。

filteredItemsのメモ化 検索語やカテゴリが変わったときだけフィルタリングが実行されます。

categoriesのメモ化 アイテムリストが変わったときだけカテゴリ一覧が再生成されます。

入力欄にタイピングしても、関係のない処理は実行されません。 これにより、スムーズなユーザー体験を提供できます。

ダッシュボードの統計情報

複雑な統計計算を行うダッシュボードの例を見てみましょう。

function Dashboard({ salesData, userData }) {
  // 売上統計の計算をメモ化
  const salesStats = useMemo(() => {
    const totalRevenue = salesData.reduce((sum, sale) => sum + sale.amount, 0);
    const averageOrderValue = totalRevenue / salesData.length;
    const topProducts = salesData
      .reduce((acc, sale) => {
        const product = acc.find(p => p.id === sale.productId);
        if (product) {
          product.sales += sale.amount;
        } else {
          acc.push({ id: sale.productId, sales: sale.amount });
        }
        return acc;
      }, [])
      .sort((a, b) => b.sales - a.sales)
      .slice(0, 5);
    
    return {
      totalRevenue,
      averageOrderValue,
      topProducts
    };
  }, [salesData]);
  
  // ユーザー統計の計算をメモ化
  const userStats = useMemo(() => {
    const activeUsers = userData.filter(user => user.isActive).length;
    const newUsers = userData.filter(user => {
      const joinDate = new Date(user.joinedAt);
      const thirtyDaysAgo = new Date();
      thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
      return joinDate > thirtyDaysAgo;
    }).length;
    
    return {
      totalUsers: userData.length,
      activeUsers,
      newUsers
    };
  }, [userData]);
  
  return (
    <div>
      <h2>売上統計</h2>
      <p>総売上: ¥{salesStats.totalRevenue.toLocaleString()}</p>
      <p>平均注文額: ¥{salesStats.averageOrderValue.toLocaleString()}</p>
      
      <h2>ユーザー統計</h2>
      <p>総ユーザー数: {userStats.totalUsers}</p>
      <p>アクティブユーザー: {userStats.activeUsers}</p>
      <p>新規ユーザー(30日以内): {userStats.newUsers}</p>
    </div>
  );
}

この例では、複雑な統計計算を2つのuseMemoに分けています。

salesStatsの計算 売上データが変わったときだけ実行されます。 総売上、平均注文額、トップ商品の計算を含みます。

userStatsの計算 ユーザーデータが変わったときだけ実行されます。 アクティブユーザー数や新規ユーザー数を計算します。

どちらの統計も、関係のないデータが変わっても再計算されません。

フォームの価格計算機能

ECサイトでよくある価格計算機能を見てみましょう。

function PriceCalculator({ products, discountRules, taxRate }) {
  const [selectedProducts, setSelectedProducts] = useState([]);
  const [couponCode, setCouponCode] = useState('');
  
  // 価格計算をメモ化
  const calculation = useMemo(() => {
    // 選択された商品の合計
    const subtotal = selectedProducts.reduce((sum, productId) => {
      const product = products.find(p => p.id === productId);
      return sum + (product ? product.price : 0);
    }, 0);
    
    // 割引の適用
    let discount = 0;
    if (couponCode) {
      const rule = discountRules.find(r => r.code === couponCode);
      if (rule) {
        discount = rule.type === 'percentage' 
          ? subtotal * (rule.value / 100)
          : rule.value;
      }
    }
    
    // 数量割引の適用
    const quantityDiscount = selectedProducts.length >= 5 ? subtotal * 0.1 : 0;
    
    const totalDiscount = discount + quantityDiscount;
    const discountedSubtotal = subtotal - totalDiscount;
    const tax = discountedSubtotal * taxRate;
    const total = discountedSubtotal + tax;
    
    return {
      subtotal,
      discount: totalDiscount,
      tax,
      total
    };
  }, [selectedProducts, products, couponCode, discountRules, taxRate]);
  
  return (
    <div>
      <h2>価格計算</h2>
      <p>小計: ¥{calculation.subtotal.toLocaleString()}</p>
      <p>割引: -¥{calculation.discount.toLocaleString()}</p>
      <p>税金: ¥{calculation.tax.toLocaleString()}</p>
      <p><strong>合計: ¥{calculation.total.toLocaleString()}</strong></p>
    </div>
  );
}

この価格計算は以下の要素を考慮しています。

小計の計算:選択された商品の合計金額 クーポン割引:入力されたクーポンコードの適用 数量割引:5個以上購入時の割引 税金計算:割引後の金額に対する税金

これらの計算は、関連する値が変わったときだけ実行されます。 フォームの他の部分が変わっても、価格計算は再実行されません。

パフォーマンス最適化のコツ

依存配列を正しく管理しよう

useMemoを効果的に使うためには、依存配列の適切な管理が重要です。

function OptimizedComponent({ data, config }) {
  // ❌ オブジェクト全体を依存配列に入れる
  const badResult = useMemo(() => {
    return expensiveCalculation(data.values);
  }, [data]); // dataが変わるたびに再計算
  
  // ✅ 必要な値のみを依存配列に入れる
  const goodResult = useMemo(() => {
    return expensiveCalculation(data.values);
  }, [data.values]); // valuesが変わったときだけ再計算
  
  // ✅ 複数の値を個別に指定
  const betterResult = useMemo(() => {
    return complexCalculation(data.values, config.threshold);
  }, [data.values, config.threshold]);
  
  return <div>{goodResult}</div>;
}

ポイント

  • オブジェクト全体ではなく、必要なプロパティのみを指定する
  • 複数の値が必要な場合は、個別に依存配列に含める
  • 不要な再計算を避けるため、最小限の依存関係にする

useCallbackとの組み合わせ

useCallbackと組み合わせることで、より効果的な最適化ができます。

import { useMemo, useCallback } from 'react';

function DataProcessor({ rawData }) {
  // 処理関数をメモ化
  const processData = useCallback((data) => {
    return data.map(item => ({
      ...item,
      processed: true,
      timestamp: Date.now()
    }));
  }, []);
  
  // 処理結果をメモ化
  const processedData = useMemo(() => {
    return processData(rawData);
  }, [rawData, processData]);
  
  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

この例では、処理関数自体もメモ化しています。

processData関数は一度作成されると、コンポーネントが再レンダリングされても同じ関数を使い回します。 これにより、useMemoの依存配列も安定し、より効果的な最適化ができます。

デバッグで効果を確認しよう

useMemoが正しく動作しているかデバッグしてみましょう。

function DebuggableComponent({ data }) {
  // 計算の実行回数をトラッキング
  const expensiveResult = useMemo(() => {
    console.log('重い計算が実行されました', Date.now());
    console.log('データサイズ:', data.length);
    
    const start = performance.now();
    const result = data.reduce((acc, item) => {
      // 複雑な計算処理
      return acc + item.value * item.multiplier;
    }, 0);
    const end = performance.now();
    
    console.log('計算時間:', end - start, 'ms');
    return result;
  }, [data]);
  
  return <div>結果: {expensiveResult}</div>;
}

この方法で以下のことがわかります。

実行回数:コンソールの出力回数で、計算が何回実行されたかわかります 実行時間performance.now()で計算にかかった時間を測定できます データサイズ:処理するデータの大きさを確認できます

デバッグ情報を見ることで、useMemoが期待通りに動作しているか確認できます。

よくある間違いと対処法

間違い1:依存配列の指定ミス

依存配列を正しく指定しないと、期待通りに動作しません。

// ❌ 依存配列を空にする
const result = useMemo(() => {
  return data.map(item => item.value * multiplier);
}, []); // dataやmultiplierが変わっても更新されない

// ✅ 適切な依存配列を指定
const result = useMemo(() => {
  return data.map(item => item.value * multiplier);
}, [data, multiplier]);

対処法

  • useMemo内で使用するすべての変数を依存配列に含める
  • ESLintのreact-hooks/exhaustive-depsルールを有効にする
  • 定期的に依存配列の内容を見直す

間違い2:過度な最適化

軽い処理にuseMemoを使うと、かえってパフォーマンスが悪くなります。

// ❌ 軽い処理にuseMemoを使う
const simpleResult = useMemo(() => {
  return a + b;
}, [a, b]);

// ✅ 重い処理にのみ使う
const complexResult = useMemo(() => {
  return heavyCalculation(largeDataSet);
}, [largeDataSet]);

対処法

  • プロファイリングツールで実際のパフォーマンスを測定する
  • 計算にかかる時間が数ミリ秒以下なら、useMemoは不要
  • 「重い」と感じる処理にのみ使用する

間違い3:不安定な依存関係

毎回新しいオブジェクトを作成すると、メモ化の効果がありません。

// ❌ 毎回新しいオブジェクトを作る
function BadComponent({ items }) {
  const options = { sort: true, filter: true }; // 毎回新しいオブジェクト
  
  const result = useMemo(() => {
    return processItems(items, options);
  }, [items, options]); // optionsが毎回変わるため意味がない
  
  return <div>{result}</div>;
}

// ✅ 安定した依存関係
function GoodComponent({ items }) {
  const options = useMemo(() => ({ sort: true, filter: true }), []);
  
  const result = useMemo(() => {
    return processItems(items, options);
  }, [items, options]);
  
  return <div>{result}</div>;
}

対処法

  • オブジェクトや配列は、別のuseMemoでメモ化する
  • 定数はコンポーネントの外で定義する
  • useCallbackで関数をメモ化する

まとめ

React useMemoは計算結果をキャッシュして、パフォーマンスを向上させる強力なツールです。

useMemoが効果的な場面

  • 重い計算処理(統計情報の算出など)
  • 配列のフィルタリングやソート
  • 複雑なオブジェクトの作成

使用時の注意点

  • 軽い処理には使わない
  • 依存配列を正しく指定する
  • 不安定な依存関係を避ける

ベストプラクティス

  • 必要な値のみを依存配列に含める
  • useCallbackとの組み合わせを活用する
  • デバッグ機能で効果を確認する

適切にuseMemoを使うことで、ユーザーが快適に使えるReactアプリケーションを作ることができます。

最初は「どこで使えばいいかわからない」と思うかもしれません。 でも実際に試してみると、その効果を実感できるはずです。

ぜひ実際のプロジェクトでuseMemoを試してみて、アプリケーションのパフォーマンス改善に役立ててください!

関連記事