【React Hooks】useMemoの使い方・メリット・注意点をわかりやすく解説!

当サイトでは一部リンクに広告が含まれています
Reactアイコン

使い勝手の良いアプリケーションを開発する上で、速度は非常に重要な指標の一つです。
Google が Web サイトを評価する際にも、速度は重要な指標の一つとして扱われています。

また、遅いアプリケーションはユーザーに嫌われ、開発者としても、性能の低いコードを書きたくはありません。

そこで React で重要なのが、再レンダリングがどの程度パフォーマンスに影響を及ぼすかという点です。

このため、ReactではuseMemoというHookが提供されています。
これは、時間とリソースを多く消費する関数の計算結果を保存(メモ化)し、再レンダリングの性能を向上させるためのものです。

目次

useMemoとは?

useMemoは、Reactの特別な機能で、複雑な計算を再度実行することなく、その結果を保存するためのものです。

この結果は再レンダリング間で保持され、その依存性の配列内の値が変更された場合にのみ再計算されます。

具体的なコード例を見てみましょう。

const a = 2;
const b = 3;

const sumOfSquares = useMemo(() => {
  return a*a + b*b;
}, [a, b]);

console.log(sumOfSquares); // 出力: 13

この例では、abの二乗の合計を求める計算が行われ、その結果がuseMemoによってメモ化(保存)されています。

そしてこの計算は、aまたはbが変更されるまで再度実行されません。
なぜなら、変数が変更されない限り、計算結果は変わらないからです。

useMemoのメリット

useMemoの最大のメリットは、再レンダリングのパフォーマンスを改善することにあります。

コストの高い関数が頻繁に呼び出されると、アプリケーション全体のパフォーマンスに影響を及ぼす可能性があります。
この影響は特に、頻繁に更新される大規模なアプリケーションで顕著です。

しかしuseMemoを使用すれば、関数の結果をメモ化し、再レンダリング時に不必要な再計算を避けることができます。
その結果、アプリケーション全体のパフォーマンスが向上します。

また、少し難しい話にはなるのですがuseMemoは参照の一貫性も保つことができます。
(参照の一貫性とは、あるオブジェクトが参照しているオブジェクトが変更された場合、そのオブジェクトも変更されることを意味します。)

これは、子コンポーネントにオブジェクトや配列などのpropsを渡す際に特に役立ちます。
なぜなら、そのpropsに依存するeffectやmemoを使用している子コンポーネントが、参照の不一致により不必要な再レンダリングを引き起こす可能性があるからです。

※参照の一貫性については少し難しいので、読み飛ばしても問題ありません。

useMemoの使い方

それでは、具体的なuseMemoの使い方と、どういう場合にメリットがあるのかを見ていきましょう。

クリックするたびにカウントアップする、カウンターコンポーネントの作成について考えてみます。

import React, { useState, useMemo } from 'react';

// countの値に応じて計算量が増える関数(コストの高い関数)
const expensiveComputation = (counterValue) => {
  let result = 0;
  for (let i = 0; i < counterValue * 1000000; i++) {
    result += i;
  }
  return result;
}

// 子コンポーネント
const Counter = () => {
  const [count, setCount] = useState(0);

  // ここで計算結果をメモ化
  const computedValue = useMemo(() => {
    return expensiveComputation(count);
  }, [count]);

  return (
    <div>
      <h1>数値 {count} の計算結果: {computedValue}</h1>
      <button onClick={() => setCount(count + 1)}>増加</button>
    </div>
  );
}

// 親コンポーネント
const App = () => {
  const [text, setText] = useState('');

  return (
    <div>
      <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
      <Counter />
    </div>
  );
}

export default App;

上記のコードでは、親コンポーネントであるApptext状態が変わると、全体の再レンダリングが発生します。
これにより、子コンポーネントであるCounterも同時に再レンダリングされます。

ただし、この再レンダリングが発生しても、Countercount状態が変わっていない場合、useMemoによって前回の計算結果(computedValue)が保持され、expensiveComputationは再実行されません。
これにより、不要な計算を省くことができ、結果的にパフォーマンスの向上に寄与します。

この例からわかるように、useMemoフックは、レンダリングのパフォーマンスを最適化するための強力な道具となることができます。
特に高コストな計算を含むコンポーネントの再レンダリングを効率的に制御する場面でその価値を発揮します。

useMemoの注意点とその対処法

メモ化された関数は、再レンダリング間で保持される

useMemoでメモ化された関数は、再レンダリング間で保持されます。
そのため、メモ化された関数は、再レンダリング時に再計算されることはありません。

しかし、依存配列が適切に設定されていない場合、意図しない挙動が発生することがあります。

以下に、適切でないuseMemoの使用例とその修正例を示します。

NGな例:

const App = () => {
  // countの値が変更されるたびに再計算される
  const [count, setCount] = useState(0);

  // 依存配列が空なので、countが変更されても再計算されない
  const doubleCount = useMemo(() => {
    return count * 2;
  }, []);

  return (
    <div>
      <h1>{count} の2倍は {doubleCount} です</h1>
      <button onClick={() => setCount(count + 1)}>増加</button>
    </div>
  );
}

上記の例では、依存配列が空なのでdoubleCountは初回レンダリング時にしか再計算されません。

以下に修正例を示します。

修正例:

const App = () => {
  const [count, setCount] = useState(0);

  // countの値が変更されるたびに再計算される
  const doubleCount = useMemo(() => {
    return count * 2;
  }, [count]);  // 依存配列にcountを追加

  return (
    <div>
      <h1>{count} の2倍は {doubleCount} です</h1>
      <button onClick={() => setCount(count + 1)}>増加</button>
    </div>
  );
}

この修正例では、useMemoの依存配列にcountを追加しています。

これにより、countの値が変更されたときにのみ、doubleCountが再計算されます。

useMemo は必ずしもパフォーマンスを改善するわけではない

useMemoを適切に使用するとパフォーマンスが改善することがありますが、不適切に使用するとパフォーマンスが逆に低下する可能性もあります。
これは、useMemo自体がリソースを消費するからです。

useMemoは計算結果をメモリに保持し、依存性配列を監視します。
そのため、複雑な計算を行っていない場合や頻繁に依存性配列の値が変更される場合などは、useMemoを使用することでむしろパフォーマンスが低下する可能性があります。

また、useMemoによってメモリ使用量が増加するという副作用も考慮に入れる必要があります。
つまり、useMemoは重い計算をする関数や参照の一貫性を保つ必要がある場合など、必要に応じて慎重に使用すべきツールであると言えます。

「とりあえずuseMemoを使っておけばパフォーマンスが改善される」というわけではないので、注意が必要です。

useMemouseCallbackの違いは?

useMemouseCallbackはともにメモ化(保存)を行うフックですが、それぞれ少し違った目的で使用されます。

  • useMemo計算結果をメモ化します。つまり、関数の実行結果を保存しておき、次回以降のレンダリング時にその保存された結果を再利用します。
  • useCallback関数自体をメモ化します。つまり、特定の関数を保存しておき、その関数が変更されない限り同じ関数を再利用します。

下記の例でそれぞれの使い方を見てみましょう。

useMemoの例:

const ExpensiveComponent = () => {
  const [count, setCount] = useState(0);

  // countの値が変更されるたびに再計算される
  const expensiveValue = useMemo(() => {
    let i = 0;
    while (i < 1000000000) i++;  // 高コストな計算
    return count * 2;  // 計算結果を返す
  }, [count]);  // countが変更されたときだけ再計算する

  return (
    <div>
      <h1>{count} の2倍は {expensiveValue} です</h1>
      <button onClick={() => setCount(count + 1)}>増加</button>
    </div>
  );
}

useCallbackの例:

const CallbackComponent = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {  // increment関数をメモ化
    setCount(count => count + 1);
  }, []);  // 依存配列が空なので、関数は一度だけ作成される

  return (
    <div>
      <h1>カウント: {count}</h1>
      <button onClick={increment}>増加</button>
    </div>
  );
}

上記のuseCallbackの例では、increment関数自体がメモ化されています。

useMemouseCallbackの両方とも、再計算や再作成を回避することでパフォーマンスの改善に貢献できますが、適切な状況と使用方法を理解することが重要です。

まとめ

ReactのuseMemoフックは、高コストの計算結果をメモ化し、不必要な再計算を避けることでパフォーマンスを向上させることができます。
しかし、useMemoは必ずしもパフォーマンスを改善するわけではなく、不適切な使用は逆にパフォーマンスを低下させたり、メモリ使用量を増加させる可能性があります。

また、useMemoと類似のuseCallbackとの違いを理解することも重要で、これらのフックを適切に使用することでReactアプリケーションのパフォーマンスを最大化することができます。

useMemoを正しく理解し、適切に使用することで、さらに効率的でパフォーマンスが高いReactアプリを開発しましょう!

React を効率的に身につける勉強法は?

React には興味があっても、プログラミング学習をはじめたばかりであれば、以下のような疑問を持つかもしれません。

  • Reactエンジニアとして活躍するために何をすればいい?
  • 何から勉強すればいいんだろう?
  • 絶対に転職を成功させるには?

React は習得が比較的難しいため、勉強法の選び方を間違うと時間・労力・お金が無駄になってしまいます…。

そのため、こちらの記事を参考に、ご自身に最適な学習方法を選んでみませんか?

目次