ウェブエンジニア問題集
第8章

useMemo — 計算結果のメモ化

6
この章の目次開く

30秒解説 — useMemoとは

結論から言うと、useMemo依存配列の値が変わらない限り前回の計算結果を使い回す(メモ化) フックです。 コンポーネントが再レンダーされても、依存する値が変わっていなければ重い計算をスキップできます。

メモ化とは、ここではパフォーマンス向上のために計算結果をキャッシュすることを意味します。

useMemo は2つの引数を受け取ります。

const value = useMemo(
  () => calculateValue(), // 第1引数: 計算して値を返す関数
  [deps], // 第2引数: 再計算する条件を並べた依存配列
);
tsx
  • 第1引数 は、メモ化したい値を計算して返す関数です。
  • 第2引数 は、その値を作り直す条件になる依存配列です。

依存配列の中身が前回と同じなら第1引数の関数は実行されず、前回の値が返ります。依存配列の中身が変わったら、第1引数の関数をもう一度実行して新しい値を返します。

学習者学習者

速くなるって聞いて全部 useMemo で包んでるけど…これって本当に意味あるの?


詳細解説 — 再レンダーのたびに何が起きるか

Reactのレンダーとは、コンポーネント関数を呼び出してUIの計算結果(JSX)を得ることでした。 stateが変わるとコンポーネント関数が再実行され、これを再レンダーと呼びます。関数が再実行されるということは、関数内のすべてのコードがもう一度走るということです。

function UserList({ users }: { users: User[] }) {
  // 再レンダーのたびに、このソート処理が毎回走る
  const sorted = users.slice().sort((a, b) => a.name.localeCompare(b.name));
 
  return (
    <ul>
      {sorted.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
tsx

ユーザーが10人程度なら毎回ソートしても問題ありません。しかし10,000人のソートを毎レンダーで走らせると、UIがもたつく原因になります。


useMemoの基本

import { useMemo } from 'react';
 
function UserList({ users }: { users: User[] }) {
  const sorted = useMemo(
    () => users.slice().sort((a, b) => a.name.localeCompare(b.name)),
    [users], // usersが変わったときだけ再計算する
  );
 
  return (
    <ul>
      {sorted.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
tsx

useMemo に渡すのは「値を返す関数」と「依存配列」です。 依存配列の中身が前回と同じなら関数は実行されず、キャッシュされた前回の計算結果がそのまま返ります。


useEffectとの比較

useMemouseEffect はどちらも依存配列を持ち、「値が変わったら何かする」という判定の仕組みは同じです。しかし「何をするか」と「いつやるか」がまったく異なります。

実行タイミングの違い

useMemo はレンダー中に同期的に実行されます。コンポーネント関数の実行中に「この値が今すぐ必要だから計算する。ただし依存が前回と同じならキャッシュを返す」という動きです。

useEffect はレンダーが終わってDOMに反映されたあとに実行されます。値を返すためのものではなく、外部との同期のためのものです。

function Example({ userId }: { userId: string }) {
  // useMemo — レンダー中に同期的に計算。結果はすぐJSXで使える。
  const displayName = useMemo(() => {
    return computeExpensiveName(userId);
  }, [userId]);
 
  // useEffect — レンダー後に非同期で実行。結果が届くのは次の再レンダー以降。
  useEffect(() => {
    fetchUserProfile(userId).then(setProfile);
  }, [userId]);
 
  return <p>{displayName}</p>;
}
tsx

判断基準

レンダー中に値が欲しいなら useMemo、レンダー後に外部と何かやりとりしたいなら useEffect です。


useRefとの比較

useRefuseMemo も再レンダーを起こさずに値を保持しますが、用途が違います。

useRef は値をそのまま保持し続けます。自分で .current を書き換えるまで変わりません。 useMemo は依存配列が変わったら自動的に再計算します。「派生データ」(元のデータから計算で得られるデータ)のキャッシュに向いています。

// useRef — 自分で書き換えるまでずっと同じ値
const timerIdRef = useRef<number | null>(null);
 
// useMemo — usersが変わったら自動的にソートし直す
const sorted = useMemo(() => sortUsers(users), [users]);
tsx

useRef は「値を保管するロッカー」、useMemo は「材料が変わったら自動で作り直してくれるキャッシュ」とイメージすると違いが掴みやすいです。


useMemoで参照を安定させる

メモ化で無駄な計算を省くイメージ

07章で触れた「オブジェクトが依存配列に入るとエフェクトが毎回走る」問題を、useMemo で解決できます。

// useMemoなし → レンダーのたびに新しいオブジェクト
// → useEffectの依存配列が毎回「変わった」と判定される
function Dashboard({ filters }: { filters: { status: string } }) {
  const params = { status: filters.status, limit: 10 };
 
  useEffect(() => {
    fetchData(params).then(setData);
  }, [params]); // paramsが毎回新しいのでエフェクトも毎回走る
}
tsx
// useMemoあり → filters.statusが変わったときだけ新しいオブジェクト
function Dashboard({ filters }: { filters: { status: string } }) {
  const params = useMemo(() => ({ status: filters.status, limit: 10 }), [filters.status]);
 
  useEffect(() => {
    fetchData(params).then(setData);
  }, [params]); // paramsの参照が安定する
}
tsx

ただし、07章で述べたようにプリミティブ値を直接依存配列に入れる方がシンプルです。useMemo でオブジェクトの参照を安定させるのは、オブジェクト自体をエフェクト以外でも使う場合の選択肢です。


使うべきか迷ったときの判断基準

useMemo はあくまで最適化の手段であり、正しさのためのものではありません。useMemo がなくてもアプリは正しく動きます。

使う場面

計算が明らかに重い場合。 大量データのソート、フィルタリング、集計、複雑な変換処理など。

// 10,000件のデータをフィルタ + ソート → メモ化する価値がある
const results = useMemo(() => {
  return items
    .filter((item) => item.category === selectedCategory)
    .sort((a, b) => b.score - a.score);
}, [items, selectedCategory]);
tsx

オブジェクトや配列の参照を安定させたい場合。 useEffect の依存配列に入れるオブジェクト、React.memo された子コンポーネントに渡すpropsなど。

使わなくてよい場面

単純な計算。 四則演算、文字列結合、短い配列の操作などはメモ化のオーバーヘッドの方が大きい場合があります。

// メモ化不要 — テンプレートリテラルの結合は一瞬で終わる
const fullName = `${firstName} ${lastName}`;
 
// メモ化不要 — 5件の配列のmapは十分高速
const labels = items.map((item) => item.label);
tsx

プリミティブ値を返す場合。 数値や文字列は参照比較の問題がないので、参照安定化の目的では不要です。


「とりあえずuseMemo」が逆効果になるケース

学習者学習者

「とりあえず全部 useMemo で包む」はダメってこと?メモ化にデメリットってあるの?

先生先生

useMemo にも依存配列の比較や値の保管というコストがある。計算が軽い処理にまでメモ化をかけると、メモ化のオーバーヘッドの方が大きくなることもあるんだ。

useMemo 自体にもコストがあります。依存配列の比較処理、前回の値の保管、そしてコードの可読性の低下です。

計算コストが十分に低い場合、メモ化のオーバーヘッドが計算のやり直しより高くつくことがあります。

// 過剰なメモ化 — 読みにくくなるだけで効果がない
const greeting = useMemo(() => `こんにちは、${name}さん`, [name]);
 
// そのまま書いた方がよい
const greeting = `こんにちは、${name}さん`;
tsx

迷ったら「まずメモ化なしで書く → 実際に遅いと感じたら計測してボトルネックを特定 → 必要な箇所にだけ入れる」の順序で進めてください。


まとめ

useMemo は依存配列の値が変わらない限り前回の計算結果を使い回すフックです。レンダー中に同期的に実行される点が useEffect と異なり、依存が変わったら自動再計算される点が useRef と異なります。

使うべき場面は「計算が明らかに重い」か「オブジェクトの参照を安定させたい」場合です。

単純な計算やプリミティブ値にはメモ化は不要で、かえってコードが読みにくくなります。

Reactクイズに挑戦するuseMemo、重い計算のキャッシュ、参照の安定、過剰なメモ化の判断基準を4択クイズで確認しよう