React 速習チュートリアル

React useMemo Hook

1. useMemoとは?

Reactの useMemo フックは、メモ化(memoized)された値を返します。

メモ化とは、値をキャッシュ(caching)して、同じ計算を繰り返さないように保存しておくことだと考えてください。useMemo フックは、依存関係(dependencies)のいずれかが更新されたときにのみ実行されます。

これにより、コンポーネントの再レンダリング(re-render)時の不要な計算を省き、パフォーマンスを向上させることができます。

1.1 useMemo と useCallback の違い

useMemouseCallback フックは非常に似ていますが、以下の明確な違いがあります:

  • useMemo: メモ化された「値」を返します。
  • useCallback: メモ化された「関数」を返します。

useCallback についての詳細は、useCallback の章を参照してください。

2. useMemoを使用しない場合

useMemo フックは、リソースを大量に消費する「重い(expensive)」関数が、レンダリングのたびに不必要に実行されるのを防ぐために使用されます。

以下の例では、レンダリングのたびに実行される重い関数が含まれています。count を変更したときはもちろんですが、todo を追加しただけでも実行に遅延(ディレイ)が発生することに気づくでしょう。

2.1 パフォーマンスの低い実装例

expensiveCalculation 関数がレンダリングのたびに実行される例です:

import { useState } from 'react';
import { createRoot } from 'react-dom/client';

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  // レンダリングごとに毎回実行される
  const calculation = expensiveCalculation(count);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "新規TODO"]);
  };

  return (
    <div>
      <div>
        <h2>マイ・TODO</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Todoを追加</button>
      </div>
      <hr />
      <div>
        カウント: {count}
        <button onClick={increment}>+</button>
        <h2>重い計算処理(Expensive Calculation)</h2>
        {calculation}
        <p>※この例では、「Todoを追加」ボタンをクリックした際も、重い関数が実行されます。</p>
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("計算中...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

createRoot(document.getElementById('root')).render(
  <App />
);

3. useMemoを使用する

このパフォーマンス問題を解決するために、useMemo フックを使用して expensiveCalculation 関数をメモ化します。これにより、必要なときにのみ関数が実行されるようになります。

重い関数の呼び出しを useMemo でラップします。useMemo フックは第2引数として依存配列を受け取り、その値(今回の場合は count)が変更されたときにのみ、重い関数を再実行します。

3.1 useMemo を使用した最適化例

以下の例では、重い関数は count が変更されたときにのみ実行され、todos が追加された際には実行されません。

import { useState, useMemo } from 'react';
import { createRoot } from 'react-dom/client';

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  
  // countが更新されたときのみexpensiveCalculationを実行し、結果をキャッシュする
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "新規TODO"]);
  };

  return (
    <div>
      <div>
        <h2>マイ・TODO</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Todoを追加</button>
      </div>
      <hr />
      <div>
        カウント: {count}
        <button onClick={increment}>+</button>
        <h2>重い計算処理(Expensive Calculation)</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("計算中...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

createRoot(document.getElementById('root')).render(
  <App />
);