React.memoを理解する中で、再レンダリングの仕組みを整理してみた

公開日:

タグ:

  • #Next.js
  • #React

前回、再レンダリングについてと、不要な最レンダリングを防ぐ方法としてメモ化について触れました。今回は実際にどうやって不要な再レンダリングを防ぐかについて触れていきたいと思います。

React.memoとは

React.memo はコンポーネントをメモ化し、前回と同じ props が渡された場合は再レンダリングをスキップするための最適化用の仕組みです。「レンダリングを止める」のではなく、「結果が変わらないなら再実行しないようにする」という考え方に近いです。

React.memoの使用例

親コンポーネントが再レンダリングされると、その中で生成される子コンポーネントも再レンダリングされることがあります。親コンポーネントの再レンダリングに合わせて、ItemList も再レンダリングされてしまいます。

以下がその例です。

'use client';
import { useState } from 'react';

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

const items: Item[] = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  name: `Item ${i + 1}`,
  price: i * 100,
}));

export default function Page() {
  console.log('Page rendered');

  const [query, setQuery] = useState('');
  return (
    <div>
		  <label>
			  検索する:
	      <input
	        type="text"
	        value={query}
	        onChange={(e) => setQuery(e.target.value)}
	      />
      </label>
      <ItemList items={items} />
    </div>
  );
}

const ItemList = function ({ items }: { items: Item[] }) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}: ${item.price}
        </li>
      ))}
    </ul>
  );
};

上記の場合、 ItemList コンポーネントは検索のためのqueryを受け取っていませんが、inputに文字が入るたびに、親コンポーネントの再レンダリングに合わせて、 ItemList も再レンダリングされてしまいます。

その際に使用するのが memo です。

ItemList を以下のように書き換えます。

import { memo, useState } from 'react';

~

const ItemList = memo(function ({ items }: { items: Item[] }) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}: ${item.price}
        </li>
      ))}
    </ul>
  );
});

ItemList.displayName = 'ItemList';

memoItemList を囲って、 displayName を設定してあげるだけです。 (displayNameを設定するのは、React DevTools でコンポーネント名を分かりやすくするため)

これだけで、props が変わらない限り親の更新による再レンダリングをスキップできるようになります。

本当にReact.memoを使うべきかどうか

memo は使い勝手は良いですが、全てのコンポーネントに対して memo を使ってしまうと、不要な memo の実行によるパフォーマンスの低下や、コードが煩雑になり保守性を保てなくなる恐れが生まれます。

以下の例は、 memo と同じく ItemList のレンダリングは初回しか実行されません。

'use client';
import { useState } from 'react';

type Item = {
  id: number;
  name: string;
  price: number;
};
const items: Item[] = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  name: `Item ${i + 1}`,
  price: i * 100,
}));

export default function Page() {
  console.log('Page rendered');

  return (
    <ItemView>
      <ItemList items={items} />
    </ItemView>
  );
}

const ItemView = function ({ children }: { children: React.ReactNode }) {
  const [query, setQuery] = useState('');
  console.log('ItemView rendered');
  return (
    <div>
		  <label>
			  検索する:
	      <input
	        type="text"
	        value={query}
	        onChange={(e) => setQuery(e.target.value)}
	      />
      </label>
      {children}
    </div>
  );
};

const ItemList = function ({ items }: { items: Item[] }) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}: ${item.price}
        </li>
      ))}
    </ul>
  );
};

上記の場合、 ItemListPage コンポーネントから渡され、 ItemView コンポーネントに propsとして渡され描画されています。

Page コンポーネントからItemView コンポーネントに渡された children は以後、同じ React 要素(同じ参照)のまま、ItemView に渡されます。

なので、ItemView コンポーネントの state が更新されても ItemList は再レンダリングされません。

(イメージとしては Page コンポーネントに ItemList が固定されている状態)

また以前記事化したcontextも設計によっては再レンダリングの影響範囲をコントロールできます。

ReactのContextをちゃんと理解する | Notes | yskm_dev

まとめ

React.memo は「再レンダリングを防ぐもの」ではなく、「props が同じなら再レンダリングをスキップする最適化」です。

memo の使用はパフォーマンスの改善につながることもありますが、むやみやたらに使用すると逆に保守性やパフォーマンスに影響がでてしまうため、まずは memo を使う前に設計として良いか検討することが大切だと感じました。

一覧へ戻る