Reactの基礎 - React.memo

提供: MochiuWiki : SUSE, EC, PCB

概要

React.memo は、コンポーネントをメモ化するための高階コンポーネント (Higher-Order Component) である。
ラップされたコンポーネントは、propsが前回のレンダリング時と変わらない場合に再レンダリングをスキップして、キャッシュされた結果を再利用する。

Reactでは、親コンポーネントが再レンダリングされると、子コンポーネントも原則として全て再レンダリングされる。
親が頻繁に状態を更新するが、子コンポーネントのpropsは変化しないケースでは、不必要なレンダリング処理が積み重なりパフォーマンスが低下する。
React.memo はこの問題を解消し、propsが変化した場合のみ再レンダリングを実行する。

propsの比較には、デフォルトでシャローイコーリティ (浅い比較) が使用される。
比較には Object.is が用いられ、数値・文字列・真偽値等のプリミティブ値は値の同一性で比較される。
<r> 一方、オブジェクトや配列は参照の同一性で比較されるため、内容が同じでも毎回新しいオブジェクトを渡すとメモ化が無効化される。

このため、React.memo 単独ではなく、useMemouseCallback と組み合わせることが実践的なパターンとなる。
useMemo はオブジェクトや計算結果の参照を安定化し、useCallback は関数の参照を安定化することで、React.memo による比較が正しく機能するようになる。

React Compiler (2025年10月 v1.0リリース) は、コンパイル時に自動でメモ化を適用するツールであり、
React.memouseMemouseCallback の手動記述の必要性を大幅に削減する。

ただし React.memo 自体は廃止されず、カスタム比較関数が必要なケース等では引き続き有用である。


基本構文

TypeScriptにおける memo 関数のシグネチャを以下に示す。

 function memo<P extends object>(
    Component: React.ComponentType<P>,
    propsAreEqual?: (prevProps: P, nextProps: P) => boolean
 ): React.MemoExoticComponent<React.ComponentType<P>>


下表に、memo 関数の引数の説明を示す。

引数の説明
引数 説明
第1引数 ラップするコンポーネント関数を指定する。
型パラメータ P にPropsの型を渡すことで型安全になる。
第2引数 (オプション) カスタム比較関数 arePropsEqual を指定する。
true を返すと再レンダリングをスキップし、false を返すと再レンダリングを実行する。
省略した場合はデフォルトの浅い比較が使用される。


コンポーネントを memo 関数でラップする方法は主に3種類ある。

関数宣言でのラップ

関数宣言を memo でラップするパターンを以下に示す。
デバッグ時にコンポーネント名が表示されるというメリットがある。

 import { memo } from 'react';
 
 // コンポーネントのpropsの型定義
 interface GreetingProps {
    name: string;
    age: number;
 }
 
 // 関数宣言をmemoでラップ
 const Greeting = memo<GreetingProps>(function Greeting({ name, age }) {
    return <h1>Hello, {name}! Age: {age}</h1>;
 });<sup>上付き文字</sup>


アロー関数でのラップ

アロー関数を memo でラップするパターンを以下に示す。

 import { memo } from 'react';
 
 // カードコンポーネントのprops型定義
 interface CardProps {
    title: string;
    content: string;
 }
 
 // アロー関数をmemoでラップ (インラインで定義)
 const Card = memo<CardProps>(({ title, content }) => {
    return (
       <div className="card">
          <h2>{title}</h2>
          <p>{content}</p>
       </div>
    );
 });


Default Exportでのラップ

コンポーネント定義とexportを分離するパターンを以下に示す。

 import { memo } from 'react';
 
 // ボタンのprops型定義
 interface ButtonProps {
    onClick: () => void;
    label: string;
 }
 
 // 通常の関数コンポーネントとして定義
 function ActionButton({ onClick, label }: ButtonProps) {
    return <button onClick={onClick}>{label}</button>;
 }
 
 // 名前を付けてエクスポートしつつ、memoでラップしてメモ化
 export default memo<ButtonProps>(ActionButton);



浅い比較の仕組み

React.memo はデフォルトで Object.is を用いた浅い比較 (Shallow Equality) によってpropsを比較する。

プリミティブ値の比較

数値・文字列・真偽値等のプリミティブ値は、値そのものが比較される。
値が同じであれば Object.istrue を返し、再レンダリングはスキップされる。

 Object.is(3, 3)             // true  -> 再レンダリングスキップ
 Object.is("hello", "hello") // true  -> 再レンダリングスキップ
 Object.is(true, true)       // true  -> 再レンダリングスキップ
 Object.is(3, 5)             // false -> 再レンダリング実行


オブジェクト・配列の比較

オブジェクトや配列は参照 (メモリアドレス) が比較される。
内容が同一であっても、異なる参照であれば Object.isfalse を返し、再レンダリングが実行される。

 Object.is({}, {})         // false -> 再レンダリング実行 (異なる参照)
 Object.is([], [])         // false -> 再レンダリング実行 (異なる参照)
 
 const obj = { id: 1 };
 Object.is(obj, obj)       // true  -> 再レンダリングスキップ (同じ参照)


レンダリングのたびに新しいオブジェクトや配列を生成してpropsとして渡すと、React.memo によるメモ化が無効化される。
この問題を解消するには、useMemo でオブジェクト参照を安定化するか、個別のプリミティブ値として渡す必要がある。


カスタム比較関数

memo の第2引数にカスタム比較関数を渡すことにより、デフォルトの浅い比較を上書きできる。

arePropsEqualの構文

カスタム比較関数のシグネチャを以下に示す。

 (prevProps: P, nextProps: P) => boolean


戻り値の意味を以下に示す。

  • true を返す場合
    propsが同じと判断され、再レンダリングをスキップする。
  • false を返す場合
    propsが異なると判断され、再レンダリングを実行する。


カスタム比較関数を使用する場合は、全てのpropsについて責任を持って比較する必要がある。

また、深い等値比較はパフォーマンス低下を招くため、必要な部分のみを比較することが推奨される。

使用例

特定のpropsのみを比較するカスタム比較関数の定義例を以下に示す。

 import { memo } from 'react';
 
 interface UserCardProps {
    userId: number;
    name: string;
    email: string;
    lastLoginAt: Date;  // 毎回新しいDateオブジェクトが渡される可能性がある
 }
 
 // userIdとnameのみを比較し、lastLoginAtの変更では再レンダリングしない
 function areEqual(prevProps: UserCardProps, nextProps: UserCardProps): boolean {
    return (
       prevProps.userId === nextProps.userId &&
       prevProps.name === nextProps.name &&
       prevProps.email === nextProps.email
    );
 }
 
 const UserCard = memo<UserCardProps>(function UserCard({ userId, name, email }) {
    return (
       <div>
          <p>ID: {userId}</p>
          <p>名前: {name}</p>
          <p>メール: {email}</p>
       </div>
    );
 }, areEqual);



サンプルコード

基本

親コンポーネントが頻繁に再レンダリングされる場合でも、React.memo でラップした子コンポーネントはpropsが変化しない限り再レンダリングをスキップする。

 import { memo, useState, FC } from 'react';
 
 interface GreetingProps {
    name: string;
 }
 
 // memoでラップすることにより、nameが変わらない限り再レンダリングされない
 const Greeting = memo<GreetingProps>(function Greeting({ name }) {
    console.log(`Greeting rendered for ${name}`);
    return <h1>Hello, {name}!</h1>;
 });
 
 const ParentComponent: FC = () => {
    const [counter, setCounter] = useState(0);
 
    return (
       <div>
          <button onClick={() => setCounter(counter + 1)}>
             Increment ({counter})
          </button>
          {/* counter が変わっても name="Alice" は変わらないため Greeting は再レンダリングされない */}
          <Greeting name="Alice" />
       </div>
    );
 };
 
 export default ParentComponent;


リストアイテムの最適化

Todoリストのような、多数のアイテムを一覧表示するコンポーネントでは、React.memouseCallback を組み合わせることが重要である。
useCallback を使用しない場合、親コンポーネントが再レンダリングされるたびにコールバック関数の参照が変わり、React.memo によるメモ化が無効化される。

 import { memo, useState, useCallback } from 'react';
 
 interface Todo {
    id: number;
    text: string;
    completed: boolean;
 }
 
 interface TodoItemProps {
    todo: Todo;
    onToggle: (id: number) => void;
    onDelete: (id: number) => void;
 }
 
 // 各Todoアイテムをメモ化する
 const TodoItem = memo<TodoItemProps>(function TodoItem({ todo, onToggle, onDelete }) {
    console.log(`TodoItem rendered: ${todo.text}`);
    return (
       <li>
          <input
             type="checkbox"
             checked={todo.completed}
             onChange={() => onToggle(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
             {todo.text}
          </span>
          <button onClick={() => onDelete(todo.id)}>削除</button>
       </li>
    );
 });
 
 const TodoList = () => {
    const [todos, setTodos] = useState<Todo[]>([
       { id: 1, text: 'Reactを学ぶ', completed: false },
       { id: 2, text: 'TypeScriptを学ぶ', completed: false },
    ]);
 
    // useCallbackでコールバック関数の参照を安定化する
    const handleToggle = useCallback((id: number) => {
       setTodos(todos => todos.map(todo =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
       ));
    }, []);
 
    const handleDelete = useCallback((id: number) => {
       setTodos(todos => todos.filter(todo => todo.id !== id));
    }, []);
 
    return (
       <ul>
          {todos.map(todo => (
             <TodoItem
                key={todo.id}
                todo={todo}
                onToggle={handleToggle}
                onDelete={handleDelete}
             />
          ))}
       </ul>
    );
 };
 
 export default TodoList;


useMemo / useCallbackとの組み合わせ

React.memouseMemouseCallback の3つを組み合わせたパターンを以下に示す。
フィルタリングされた商品リストを表示するコンポーネントを例として用いる。

 import { memo, useState, useCallback, useMemo } from 'react';
 
 interface Product {
    id: number;
    name: string;
    price: number;
    category: string;
 }
 
 interface FilteredListProps {
    products: Product[];
    onSelect: (product: Product) => void;
 }
 
 // 1. React.memoでコンポーネントをメモ化する
 const FilteredList = memo<FilteredListProps>(function FilteredList({ products, onSelect }) {
    console.log('FilteredList rendered');
    return (
       <ul>
          {products.map(product => (
             <li key={product.id} onClick={() => onSelect(product)}>
                {product.name} - ${product.price}
             </li>
          ))}
       </ul>
    );
 });
 
 const ProductPage = () => {
    const [allProducts] = useState<Product[]>([
       { id: 1, name: 'Laptop', price: 1200, category: 'Electronics' },
       { id: 2, name: 'Keyboard', price: 80, category: 'Electronics' },
       { id: 3, name: 'Desk', price: 300, category: 'Furniture' },
    ]);
    const [selectedCategory, setSelectedCategory] = useState('Electronics');
    const [searchQuery, setSearchQuery] = useState('');
    const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
 
    // 2. useMemoでフィルタリング結果 (配列) の参照を安定化する
    const filteredProducts = useMemo(() => {
       return allProducts.filter(product =>
          product.category === selectedCategory &&
          product.name.toLowerCase().includes(searchQuery.toLowerCase())
       );
    }, [allProducts, selectedCategory, searchQuery]);
 
    // 3. useCallbackでコールバック関数の参照を安定化する
    const handleSelect = useCallback((product: Product) => {
       setSelectedProduct(product);
    }, []);
 
    return (
       <div>
          <input
             placeholder="商品を検索..."
             value={searchQuery}
             onChange={(e) => setSearchQuery(e.target.value)}
          />
          <select
             value={selectedCategory}
             onChange={(e) => setSelectedCategory(e.target.value)}
          >
             <option value="Electronics">Electronics</option>
             <option value="Furniture">Furniture</option>
          </select>
          {selectedProduct && <p>選択中: {selectedProduct.name}</p>}
          {/* filteredProducts と handleSelect の参照が安定しているため、
              searchQuery等が変わらない限り FilteredList は再レンダリングされない */}
          <FilteredList products={filteredProducts} onSelect={handleSelect} />
       </div>
    );
 };
 
 export default ProductPage;



React Compilerとの関係

React Compilerによる自動メモ化

React Compiler (旧称: React Forget) は、2025年10月にv1.0がリリースされたBabelプラグインである。
コンパイル時にソースコードを解析し、React.memouseMemouseCallback に相当する最適化を自動的に適用する。

React Compilerを導入することで得られる効果を以下に示す。

  • メモ化の手動記述が不要になる。
    useMemouseCallback を明示的に書かなくても、コンパイラが適切な箇所に自動でメモ化を挿入する。
  • コードの簡潔さが向上する。
    最適化のためのボイラープレートコードが減少して、ビジネスロジックに集中した記述ができる。
  • 段階的な導入が可能
    既存プロジェクトに対しても、ファイル単位で段階的に導入できる。


React 19 と React Compilerは別プロジェクトであり、React Compilerの導入はオプションである。
React 19を使用していても、React Compilerを導入しない限り自動メモ化は適用されない。

React.memoが引き続き必要なケース

React Compilerを使用する環境でも、React.memo が有用なケースは存在する。

  • カスタム比較関数が必要な場合
    特定のpropsのみを比較したい場合や、深いネストの一部だけを比較する場合は、カスタム比較関数を持つ React.memo が適している。
    コンパイラによる自動メモ化はカスタム比較ロジックを持たない。
  • React Compilerを導入していないプロジェクト
    React Compilerの導入はオプションであるため、多くの既存プロジェクトでは引き続き手動のメモ化が必要である。
  • 明示的な最適化ポイントの文書化
    チームの方針として、パフォーマンス上重要なコンポーネントを明示的に示したい場合に使用する。



useMemo / useCallbackとの連携

React.memo単独では不十分なケース

React.memo は浅い比較でpropsを評価するため、オブジェクトや関数をpropsとして渡すと参照が毎回変わり、メモ化が無効化される。

オブジェクトをpropsとして渡す場合の問題を以下に示す。

 import { memo, useState } from 'react';
 
 interface StyleProps {
    style: { color: string; fontSize: number };
 }
 
 const StyledText = memo<StyleProps>(function StyledText({ style }) {
    console.log('StyledText rendered');
    return <p style={style}>テキスト</p>;
 });
 
 const Parent = () => {
    const [count, setCount] = useState(0);
 
    // 問題: レンダリングのたびに新しいオブジェクトが生成される
    // memo による比較は常に false になり、毎回再レンダリングされる
    const style = { color: 'red', fontSize: 16 };
 
    return (
       <div>
          <button onClick={() => setCount(c => c + 1)}>{count}</button>
          <StyledText style={style} />
       </div>
    );
 };


この問題は、useMemo を使用してオブジェクトの参照を安定化させることで解決できる。

 import { memo, useState, useMemo } from 'react';
 
 const Parent = () => {
    const [count, setCount] = useState(0);
 
    // 解決: useMemo でオブジェクト参照を安定化する
    const style = useMemo(() => ({ color: 'red', fontSize: 16 }), []);
 
    return (
       <div>
          <button onClick={() => setCount(c => c + 1)}>{count}</button>
          <StyledText style={style} />
       </div>
    );
 };


3つのメモ化手法の使い分け

Reactが提供する3つのメモ化手法の役割と使い分けを以下に示す。

3つのメモ化手法の比較
Hook / API メモ化の対象 主な用途
React.memo コンポーネント propsが変化しない場合に再レンダリングをスキップする。
useMemo 値 (オブジェクト・配列・計算結果) 重い計算結果のキャッシュ、オブジェクト参照の安定化
useCallback 関数 コールバック関数の参照の安定化


3つの手法は組み合わせて使用することで効果を発揮する。

  • React.memo でコンポーネントをメモ化する。
    子コンポーネントを React.memo でラップして、propsが変わらない場合の再レンダリングを防ぐ。
  • useMemo でオブジェクト・配列を安定化する。
    親から子へオブジェクトや配列を渡す場合は、useMemo で参照を安定化して React.memo の比較が機能するようにする。
  • useCallback で関数を安定化する
    親から子へコールバック関数を渡す場合は、useCallback で参照を安定化して React.memo の比較が機能するようにする。



注意事項

propsにオブジェクトや関数を渡すとmemoが無効化される

React.memo を使用していても、以下に示すようなケースではメモ化が無効化される。

  • レンダリングのたびにインラインでオブジェクトを生成して渡す場合
    useMemo でオブジェクトをメモ化するか、コンポーネント外で定数として定義することで解決できる。
  • レンダリングのたびにアロー関数をインラインで定義して渡す場合
    useCallback で関数をメモ化することで解決できる。
  • propsとして渡すオブジェクトを個別のプリミティブ値に展開する方法
    オブジェクトをpropsとして渡す代わりに、各プロパティを個別のpropsとして渡すことでも解決できる。


 // 問題のあるパターン
 <MemoizedComponent
    config={{ theme: 'dark', size: 'lg' }}   // 毎回新しいオブジェクト
    onClick={() => handleClick(id)}            // 毎回新しい関数
 />
 
 // 改善したパターン
 const config = useMemo(() => ({ theme: 'dark', size: 'lg' }), []);
 const handleClickMemo = useCallback(() => handleClick(id), [id]);
 
 <MemoizedComponent config={config} onClick={handleClickMemo} />
 
 // または個別のプリミティブ値として渡す
 <MemoizedComponent theme="dark" size="lg" onClick={handleClickMemo} />


useContextを使用している場合の挙動

React.memo でラップしたコンポーネントが useContext を内部で使用している場合、
Contextの値が変更されると React.memo によるメモ化は無効化され、コンポーネントは再レンダリングされる。

これは React.memo の仕様上の制限であり、propsが変化していなくてもContextの変更には反応する。
この問題を回避するには、Contextを適切に分割して更新頻度の高い値と低い値を別々のContextに分けるか、useContext を使用しない子コンポーネントに分割してpropsとして値を渡すことが有効である。

React.memoを使うべきでない場面

React.memo は比較処理のオーバーヘッドを伴うため、全てのコンポーネントに適用すべきではない。
以下に示す場合は使用を避けることが推奨される。

  • propsがほぼ毎回変わる場合
    メモ化の効果がなく、比較のオーバーヘッドが増えるだけとなる。
  • JSXの生成が軽量なシンプルなコンポーネント
    メモ化のコストが再レンダリングのコストを上回る可能性がある。
  • React.memo 単独ではメモ化が機能しない場合
    オブジェクト・配列・関数をpropsとして渡しており、useMemouseCallback で参照を安定化していない場合は、React.memo を使っても効果がない。
  • useContext を多用している場合
    Context値の変更で頻繁に再レンダリングが発生して、React.memo の効果が得られにくい。



パフォーマンスの測定

React.memo の効果を確認するには、React DevTools Profiler または React組み込みの Profiler コンポーネントを使用する。

React DevTools Profiler

ブラウザ拡張機能のReact DevToolsに内蔵されたProfilerを使用することにより、各コンポーネントのレンダリング時間とレンダリング原因を視覚的に確認できる。

使用手順を以下に示す・

  1. React DevTools拡張機能をインストールする。
  2. Webブラウザの開発者ツールを開き、[Profiler]タブを選択する。
  3. [Record]ボタンを押下して、操作を記録する。
  4. 停止後、各コンポーネントのレンダリング回数と所要時間を確認する。


Profilerコンポーネント

Reactが提供する Profiler コンポーネントをコード内に配置することにより、プログラムからレンダリング情報を取得できる。

 import { Profiler, ProfilerOnRenderCallback, memo, useState } from 'react';
 
 interface GreetingProps {
    name: string;
 }
 
 const Greeting = memo<GreetingProps>(function Greeting({ name }) {
    return <h1>Hello, {name}!</h1>;
 });
 
 const onRenderCallback: ProfilerOnRenderCallback = (
    id,             // Profilerコンポーネントのid属性
    phase,          // "mount" (初回) | "update" (再レンダリング) | "nested-update"
    actualDuration, // 実際のレンダリング時間 (ms)
    baseDuration,   // 最適化なしの推定レンダリング時間 (ms)
    startTime,      // レンダリング開始時のタイムスタンプ
    commitTime      // コミット完了時のタイムスタンプ
 ) => {
    console.log(`[Profiler] ${id} (${phase}): ${actualDuration.toFixed(2)}ms (base: ${baseDuration.toFixed(2)}ms)`);
 };
 
 const App = () => {
    const [counter, setCounter] = useState(0);
 
    return (
       <Profiler id="Greeting" onRender={onRenderCallback}>
          <button onClick={() => setCounter(c => c + 1)}>Increment ({counter})</button>
          <Greeting name="Alice" />
       </Profiler>
    );
 };
 
 export default App;


下表に、Profiler コンポーネントの主なコールバック引数の説明を示す。

ProfilerOnRenderCallbackの引数一覧
引数 説明
id string Profiler コンポーネントに指定した識別子
phase string mount (初回レンダリング)、update (再レンダリング)、nested-update のいずれか
actualDuration number 今回のレンダリングに要した実際の時間 (ミリ秒)
baseDuration number メモ化無しで全子コンポーネントをレンダリングした場合の推定時間 (ミリ秒)
startTime number レンダリング開始時のタイムスタンプ
commitTime number コミット完了時のタイムスタンプ


actualDurationbaseDuration より大幅に小さい場合、メモ化が効果的に機能していることを表す。


関連情報