React.js

ReactのuseMemoとuseCallback:商品検索で学ぶメモ化の使い分け

この記事でわかること

ReactのuseMemo・useCallback・memoの違いを商品検索のコードで解説。依存配列、propsの参照、React Compilerとの関係、計測して最適化する判断を学びます。

useMemouseCallbackは、すべてのコンポーネントへ最初から追加するものではありません。この記事では商品検索の例を使い、計算結果を保持する場合と、関数の参照を保持する場合の違いを学びます。memoとの組み合わせ、依存配列、最適化を導入する判断も整理します。

3つの機能を区別する

useMemo・useCallback・memoの役割

  • useMemoは、依存値が変わらない間、計算した値を再利用するためのHookです。
  • useCallbackは、依存値が変わらない間、関数の参照を再利用するためのHookです。
  • memoは、propsが変わらないとき、親の更新に伴うコンポーネントの再レンダーを省略できるようにするAPIです。

いずれも最適化のための機能です。メモ化を外したら動作が壊れる場合は、最適化とは別の設計上の問題がないか確認します。レンダーの省略やキャッシュの永続保持を、アプリの正しさの前提にしません。

この記事の実行条件

ViteのReact・TypeScriptプロジェクトを使います。準備はReact.js入門を参照してください。例は手動のメモ化の仕組みを説明するもので、この小さなデータ量で高速化が必要だと主張するものではありません。

React Compilerが有効な環境では、自動的なメモ化によって手動の指定が必要な場面が減ります。ReactのバージョンだけでCompilerが有効と判断せず、プロジェクトのビルド設定を確認してください。

memoで商品一覧を定義する

ProductList.tsxを作成する

src/ProductList.tsxを作成します。商品配列と選択時の関数をpropsで受け取る一覧を、memoで包みます。

import { memo } from 'react';

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

type ProductListProps = {
  products: Product[];
  onSelect: (id: string) => void;
};

export const ProductList = memo(function ProductList({ products, onSelect }: ProductListProps) {
  if (products.length === 0) return <p>該当する商品がありません。</p>;

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <span>{product.name}:{product.price}円</span>
          <button type="button" onClick={() => onSelect(product.id)}>
            {product.name}を選ぶ
          </button>
        </li>
      ))}
    </ul>
  );
});

propsの参照が変わるとどうなるか

memoの標準の比較では、各propをObject.isで比較します。親がレンダーのたびに新しい配列や関数を作って渡すと、中身が同じように見えても参照は異なります。

子の中のonClick={() => onSelect(product.id)}まで、すべてをuseCallbackに置き換える必要はありません。この例で親子間の比較対象となるのは、productsonSelectです。

useMemoとuseCallbackを親で使う

ProductSearch.tsxを実装する

src/ProductSearch.tsxを作成します。検索語から表示する商品を計算し、選択した商品のIDをstateに保持します。商品データは例を簡単にするため、コンポーネントの外に置きます。

import { useCallback, useMemo, useState } from 'react';
import { ProductList, type Product } from './ProductList';

const products: Product[] = [
  { id: 'keyboard', name: 'キーボード', price: 6000 },
  { id: 'mouse', name: 'マウス', price: 3000 },
  { id: 'monitor', name: 'モニター', price: 24000 },
];

export function ProductSearch() {
  const [query, setQuery] = useState('');
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [showHelp, setShowHelp] = useState(false);
  const keyword = query.trim().toLocaleLowerCase('ja-JP');

  const visibleProducts = useMemo(() =>
    products.filter((product) =>
      product.name.toLocaleLowerCase('ja-JP').includes(keyword)
    ),
    [keyword]
  );

  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []);

  const selectedProduct = products.find((product) => product.id === selectedId);

  return (
    <section aria-labelledby="search-heading">
      <h2 id="search-heading">商品を検索する</h2>
      <label>
        商品名
        <input
          type="search"
          value={query}
          onChange={(event) => setQuery(event.target.value)}
        />
      </label>
      <button
        type="button"
        aria-expanded={showHelp}
        aria-controls="search-help"
        onClick={() => setShowHelp((current) => !current)}
      >
        検索のヒント
      </button>
      <p id="search-help" hidden={!showHelp}>商品名の一部を入力してください。</p>
      <p role="status">検索結果:{visibleProducts.length}件</p>
      <ProductList products={visibleProducts} onSelect={handleSelect} />
      <p role="status">選択中:{selectedProduct?.name ?? 'なし'}</p>
    </section>
  );
}

useMemoは計算結果を保持する

この例のuseMemoは、絞り込んだ配列を返します。keywordが変わると計算し直し、同じなら保持している結果を再利用できます。「検索のヒント」の開閉はkeywordを変えないため、商品配列の参照も維持できます。

商品をAPIから受け取ってpropsやstateで管理する場合は、その商品配列も依存値です。今回のproductsが依存配列にないのは、コンポーネント外の固定データだからです。実際に内容を更新するデータとして扱う場合は、配列を直接変更せず、新しい値として更新してください。

useCallbackは関数を呼び出さない

useCallbackに渡した関数は、商品を選んだときに実行されます。Hookを呼んだ時点で商品が選択されるわけではありません。

handleSelectは安定したstate更新関数だけを参照しているため、依存配列は空です。propsの関数や変わるstateを読み取る処理を追加した場合は、その値を依存配列に含めます。依存値を省略して古い値を使い続ける問題を、最適化と引き換えに持ち込まないようにします。

App.tsxから表示して確認する

コンポーネントを配置する

src/App.tsxを次の内容にします。既存のViteテンプレートのsrc/main.tsxから表示できます。

import { ProductSearch } from './ProductSearch';

export default function App() {
  return (
    <main>
      <h1>商品検索で学ぶメモ化</h1>
      <ProductSearch />
    </main>
  );
}

機能の正しさを確認する

  • 初期状態は検索結果3件、選択中は「なし」です。
  • 「マウス」で検索すると1件になります。
  • 「マウスを選ぶ」を押すと、選択中の表示が変わります。
  • 検索条件を変えても、選択中の商品は維持されます。今回の仕様では検索結果に選択を連動させて解除しません。
  • 存在しない商品名では0件の案内が表示されます。
  • ヒントを開閉しても、検索結果や選択状態は変わりません。

Viteプロジェクトのビルドも確認します。

npm run build

最適化を導入する判断と落とし穴

先に計測する

配列が数件なら、単純にfilterしても通常は十分です。まず操作の遅さを確認し、React Developer ToolsのProfilerなどで対象のレンダーを調べます。比較時は同じデータ量と操作を使い、開発時のStrict Modeによる追加実行と、本番環境の動作を区別します。

このサンプルでは、ヒント開閉時に商品一覧のpropsが維持される構造を観察できます。ただし、これだけでアプリ全体が速くなったと結論づけることはできません。

毎回作るオブジェクトを依存値にしない

レンダーのたびに新しい設定オブジェクトを作って依存配列に入れると、その参照が毎回変わるため再計算されます。今回のように必要な文字列を直接依存値にするか、計算内で設定オブジェクトを作る方法を検討します。

stateやContextの更新まで止める機能ではない

memoで包んでも、そのコンポーネント自身のstateや読み取っているContextが変われば更新されます。手動で比較関数を書く場合も、関数propsを含めて比較する必要があります。最初は独自比較を増やさず、どの値が変わっているかを調べましょう。

関連記事と参考資料

基本Hooksと組み合わせる

更新関数やstateの考え方はReactの基本Hooks入門で確認できます。メモ化は、状態の置き場所とデータの流れが整理できてから検討すると、何を再利用したいのかが明確になります。

公式ドキュメント

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →