【React】useMemoで無駄な再計算を防ぐ

JavaScript

こんにちは、かつコーチです。

前回は「useContextでProps drillingを解消する」を解説しました。

今回は、パフォーマンスチューニングの入り口として避けて通れない useMemo を扱います。

「なぜ画面がカクつくのか」「どこを最適化すればいいのか」を判断する土台になるフックなので、しっかり理解しておきましょう。

useMemoとは?

計算結果をレンダリングをまたいで覚えておく

useMemo は、重い計算処理の結果をメモ化(前回の計算結果を覚えておき、条件が変わらなければ使い回すこと)するためのフックです。

import { useMemo, useState } from "react";

function ExpensiveList({ items }: { items: number[] }) {
  const [keyword, setKeyword] = useState("");

  const total = useMemo(() => {
    console.log("合計を再計算しました");
    return items.reduce((sum, item) => sum + item, 0);
  }, [items]);

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <p>合計: {total}</p>
    </div>
  );
}

useMemo の第一引数には計算処理を返す関数、第二引数には依存配列を渡します。

依存配列(この例では [items])の中身が前回のレンダリングと変わっていなければ、計算処理は実行されず、前回の結果がそのまま返されます。

上のコードでは、keyword を変更して再レンダリングされても items が変わらない限り「合計を再計算しました」というログは出力されません。

なぜ再計算を防ぐ必要があるのか

Reactのコンポーネントは、propsやstateが変わるたびに関数全体が再実行されます。

つまり、コンポーネント内に書いた「重い計算処理」も、本来関係のない別のstateの変更のたびに毎回実行されてしまうということです。

数百件のデータをソートしたり、複雑な集計をしたりする処理がこの巻き添えを食うと、入力欄に文字を打つたびに画面がカクつく、といった体感できるレベルの遅さにつながります。

いつuseMemoを使うべきか

使うべき場面

useMemo の効果が実感しやすいのは、次のような場面です。

  • 配列のソート・フィルタリング・集計など、データ量に応じて処理時間が伸びる計算
  • 子コンポーネントに渡すオブジェクトや配列を、参照の同一性を保ったまま渡したいとき
import { useMemo, useState } from "react";

type Product = { id: number; name: string; price: number };

function ProductList({ products }: { products: Product[] }) {
  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");

  const sortedProducts = useMemo(() => {
    return [...products].sort((a, b) =>
      sortOrder === "asc" ? a.price - b.price : b.price - a.price
    );
  }, [products, sortOrder]);

  return (
    <ul>
      {sortedProducts.map((product) => (
        <li key={product.id}>
          {product.name} - {product.price}円
        </li>
      ))}
    </ul>
  );
}

productssortOrder が変わらない限り、ソート処理は再実行されません。

使わなくてよい場面

一方で、単純な文字列連結や数値の足し算のような軽い計算にまで useMemo を使うのはおすすめしません。

useMemo 自体にも「前回の依存配列と比較する」というコストがあるため、計算そのものが軽い場合はメモ化のオーバーヘッドの方が大きくなることがあります。

「その計算、本当に重いか?」を先に確認してから使う、というのが基本方針です。

つまずきやすいポイント:依存配列に入れ忘れる

かつコーチが実際にハマった例

私が useMemo を使い始めた頃、依存配列に入れるべき値を一部入れ忘れ、フィルタ条件を変えても表示が更新されないバグに遭遇しました。

❌ Before:依存配列にminPriceを入れ忘れている

import { useMemo, useState } from "react";

type Product = { id: number; name: string; price: number };

function FilteredList({ products }: { products: Product[] }) {
  const [minPrice, setMinPrice] = useState(0);

  const filtered = useMemo(() => {
    return products.filter((p) => p.price >= minPrice);
  }, [products]); // minPriceが依存配列にない

  return (
    <div>
      <input
        type="number"
        value={minPrice}
        onChange={(e) => setMinPrice(Number(e.target.value))}
      />
      <ul>
        {filtered.map((p) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

minPrice を変更しても、依存配列には products しか入っていないため、filtered は前回の計算結果を使い回してしまい、一覧が全く更新されませんでした。

「入力欄の値は変わっているのに、リストだけ古いまま」という不可解な状態にしばらく気づけず、コンソールで値を出力してようやく原因が分かりました。

✅ After:計算に使っている値はすべて依存配列に入れる

const filtered = useMemo(() => {
  return products.filter((p) => p.price >= minPrice);
}, [products, minPrice]);

計算処理の中で参照している変数(productsminPrice)をすべて依存配列に含めることで、条件が変わるたびに正しく再計算されるようになりました。

ESLintの react-hooks/exhaustive-deps ルールを有効にしておくと、こうした入れ忘れを自動で警告してくれるので、導入しておくことを強くおすすめします。

応用:useMemoで子コンポーネントの再レンダリングを抑える

オブジェクトの参照を安定させる

useMemo は計算結果のメモ化だけでなく、子コンポーネントに渡すオブジェクトの参照の同一性を保つ目的でも使われます。

import { memo, useMemo, useState } from "react";

type Filter = { keyword: string; category: string };

const FilterPanel = memo(function FilterPanel({ filter }: { filter: Filter }) {
  console.log("FilterPanelが再レンダリングされました");
  return <p>キーワード: {filter.keyword}</p>;
});

function SearchScreen() {
  const [keyword, setKeyword] = useState("");
  const [count, setCount] = useState(0);

  const filter = useMemo<Filter>(
    () => ({ keyword, category: "全て" }),
[keyword]

); return ( <div> <button onClick={() => setCount(count + 1)}>再レンダリング用: {count}</button> <FilterPanel filter={filter} /> </div> ); }

filter オブジェクトを useMemo でメモ化しておくことで、count の変更で SearchScreen が再レンダリングされても、keyword が変わらない限り新しいオブジェクトが生成されません。

memo でラップした FilterPanel は、propsの参照が変わらないため無駄な再レンダリングをスキップできます。

このパターンは次回解説する useCallback とセットで使われることが多いので、あわせて押さえておくと理解が深まります。

まとめ

この記事のポイント

  • useMemo は計算結果をレンダリングをまたいで覚えておくフック
  • ソートや集計など「重い計算」に対して使うと効果が大きい
  • 計算に使っている値は依存配列にすべて入れる必要がある
  • 子コンポーネントに渡すオブジェクトの参照を安定させる目的でも使われる

次に読むべき記事

次回は「useCallbackで関数の再生成を防ぐ」です。

useMemo とセットで理解しておきたいフックなので、続けて読んでみてください。

→ 次の記事:useCallbackで関数の再生成を防ぐ

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