memo は、プロップが変更されていない場合にコンポーネントの再レンダリングをスキップできます。

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

リファレンス

memo(Component, arePropsEqual?)

memoでコンポーネントをラップして、そのコンポーネントのメモ化されたバージョンを取得します。このメモ化されたコンポーネントバージョンは、親コンポーネントが再レンダリングされても、プロップが変更されていない限り、通常は再レンダリングされません。ただし、Reactはそれでも再レンダリングすることがあります。メモ化はパフォーマンスの最適化であり、保証ではありません。

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
// ...
});

以下の例を参照してください。

パラメーター

  • Component: メモ化するコンポーネント。 memoはこのコンポーネントを変更しません。代わりに新しいメモ化されたコンポーネントを返します。関数やforwardRefコンポーネントを含む、有効なReactコンポーネントがすべて受け入れられます。

  • オプション arePropsEqual: 2つの引数(コンポーネントの以前のプロップと新しいプロップ)を受け取る関数。古いプロップと新しいプロップが等しい場合(つまり、新しいプロップで古いプロップと同じ出力をレンダリングし、同じように動作する場合)、trueを返す必要があります。そうでない場合はfalseを返します。通常、この関数を指定することはありません。デフォルトでは、Reactは各プロップをObject.isで比較します。

戻り値

memo は新しいReactコンポーネントを返します。これは、memo に提供されたコンポーネントと同じ動作をします。ただし、親コンポーネントが再レンダリングされる場合でも、propsが変更されない限り、Reactは常に再レンダリングするとは限りません。


使用方法

propsが変更されない場合の再レンダリングのスキップ

Reactは通常、親コンポーネントが再レンダリングされるたびにコンポーネントを再レンダリングします。memo を使用すると、親コンポーネントが再レンダリングされても、新しいpropsが古いpropsと同じである限り、Reactが再レンダリングしないコンポーネントを作成できます。このようなコンポーネントは、メモ化されていると言われます。

コンポーネントをメモ化するには、memo でラップし、元のコンポーネントの代わりに返された値を使用します。

const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

Reactコンポーネントは常にピュアなレンダリングロジックを持つ必要があります。これは、props、state、およびcontextが変更されていない場合、同じ出力を返す必要があることを意味します。memo を使用することにより、コンポーネントがこの要件を満たしていることをReactに伝え、propsが変更されていない限り、Reactは再レンダリングする必要がなくなります。memo を使用していても、独自のstateが変更された場合、または使用しているcontextが変更された場合は、コンポーネントは再レンダリングされます。

この例では、name が変更されると(これはpropsの1つであるため)、Greeting コンポーネントは再レンダリングされますが、address が変更されても再レンダリングされません(Greeting にpropsとして渡されていないためです)。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return <h3>Hello{name && ', '}{name}!</h3>;
});

注記

memo はパフォーマンス最適化としてのみ使用する必要があります。 コードがこれなしでは動作しない場合は、根本的な問題を見つけて最初に修正してください。その後、パフォーマンスを向上させるためにmemo を追加できます。

詳細

すべての場所にmemoを追加する必要がありますか?

アプリケーションがこのサイトのように、ほとんどの操作が粗粒度(ページ全体またはセクション全体の置換など)である場合、メモ化は通常不要です。一方、アプリケーションが図面エディターのように、ほとんどの操作が細粒度(図形の移動など)である場合、メモ化は非常に役立つ場合があります。

memo を使用した最適化は、コンポーネントが同じpropsで頻繁に再レンダリングされ、その再レンダリングロジックがコスト高である場合にのみ価値があります。コンポーネントの再レンダリング時に目に見える遅延がない場合は、memo は不要です。memo は、コンポーネントに渡されるpropsが常に異なる場合(レンダリング時に定義されたオブジェクトまたはプレーンな関数を渡す場合など)は完全に役に立ちません。そのため、多くの場合、useMemouseCallbackmemo と組み合わせて使用する必要があります。

他のケースでは、memo でコンポーネントをラップすることには利点はありません。そうすることには大きな害はありませんが、一部のチームは個々のケースを考えずに、できるだけ多くのメモ化を選択しています。このアプローチの欠点は、コードが読みづらくなることです。また、すべてのメモ化が効果的であるとは限りません。「常に新しい」単一の値は、コンポーネント全体のメモ化を壊すのに十分です。

実際には、いくつかの原則に従うことで、多くのメモ化を不要にすることができます。

  1. コンポーネントが他のコンポーネントを視覚的にラップする場合、JSXを子として受け入れるようにします。これにより、ラッパーコンポーネントが独自のstateを更新すると、Reactは子コンポーネントを再レンダリングする必要がないことを認識します。
  2. ローカルstateを優先し、stateを上位に持ち上げることを必要以上にしないでください。たとえば、フォームやアイテムがホバーされているかなどの一時的なstateをツリーの一番上に、またはグローバルstateライブラリに保持しないでください。
  3. レンダリングロジックをピュアに保ってください。 コンポーネントの再レンダリングが問題を引き起こしたり、目に見える視覚的なアーティファクトを作成したりする場合は、コンポーネントのバグです。メモ化を追加するのではなく、バグを修正してください。
  4. stateを更新する不要なEffectを避けてください。Reactアプリケーションのパフォーマンス上のほとんどの問題は、コンポーネントを何度もレンダリングさせるEffectから発生する更新の連鎖によって引き起こされます。
  5. Effectから不要な依存関係を削除してください。 たとえば、メモ化の代わりに、Effect内またはコンポーネントの外にオブジェクトや関数を移動する方が簡単なことがよくあります。

特定のインタラクションが依然として遅いと感じる場合は、React Developer Toolsプロファイラーを使用して、どのコンポーネントがメモ化から最も恩恵を受けるかを確認し、必要に応じてメモ化を追加してください。これらの原則は、コンポーネントのデバッグと理解を容易にするため、いずれの場合でも従うことが推奨されます。長期的に、私たちは細粒度のメモ化を自動的に行うことを研究して、この問題を一度に解決することを目指しています。


状態を使用してメモ化されたコンポーネントを更新する

コンポーネントがメモ化されていても、独自の state が変更されると再レンダリングされます。メモ化は、親コンポーネントからコンポーネントに渡される props のみに関係します。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log('Greeting was rendered at', new Date().toLocaleTimeString());
  const [greeting, setGreeting] = useState('Hello');
  return (
    <>
      <h3>{greeting}{name && ', '}{name}!</h3>
      <GreetingSelector value={greeting} onChange={setGreeting} />
    </>
  );
});

function GreetingSelector({ value, onChange }) {
  return (
    <>
      <label>
        <input
          type="radio"
          checked={value === 'Hello'}
          onChange={e => onChange('Hello')}
        />
        Regular greeting
      </label>
      <label>
        <input
          type="radio"
          checked={value === 'Hello and welcome'}
          onChange={e => onChange('Hello and welcome')}
        />
        Enthusiastic greeting
      </label>
    </>
  );
}

state 変数を現在の値に設定した場合、React は memo を使用しなくても、コンポーネントの再レンダリングをスキップします。コンポーネント関数が余分な回数を呼び出されているように見える場合がありますが、その結果は破棄されます。


コンテキストを使用してメモ化されたコンポーネントを更新する

コンポーネントがメモ化されていても、使用しているコンテキストが変更されると再レンダリングされます。メモ化は、親コンポーネントからコンポーネントに渡される props のみに関係します。

import { createContext, memo, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('dark');

  function handleClick() {
    setTheme(theme === 'dark' ? 'light' : 'dark'); 
  }

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={handleClick}>
        Switch theme
      </button>
      <Greeting name="Taylor" />
    </ThemeContext.Provider>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  const theme = useContext(ThemeContext);
  return (
    <h3 className={theme}>Hello, {name}!</h3>
  );
});

コンテキストの一部のみが変更された場合にのみコンポーネントを再レンダリングするには、コンポーネントを2つに分割します。外部コンポーネントでコンテキストから必要なものを読み取り、それを props としてメモ化された子コンポーネントに渡します。


props の変更を最小限に抑える

memo を使用すると、すべての prop が以前のものと「浅い等価性」を持たない場合に、コンポーネントが再レンダリングされます。これは、React が Object.is 比較を使用して、コンポーネントのすべての prop を以前の値と比較することを意味します。Object.is(3, 3)true ですが、Object.is({}, {})false です。

memo を最大限に活用するには、props の変更回数を最小限に抑えます。たとえば、prop がオブジェクトの場合、useMemo を使用して、親コンポーネントが毎回そのオブジェクトを再作成するのを防ぎます。

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);

const person = useMemo(
() => ({ name, age }),
[name, age]
);

return <Profile person={person} />;
}

const Profile = memo(function Profile({ person }) {
// ...
});

props の変更を最小限に抑えるためのより良い方法は、コンポーネントが props で必要な最小限の情報を受け入れるようにすることです。たとえば、オブジェクト全体ではなく、個々の値を受け入れることができます。

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}

const Profile = memo(function Profile({ name, age }) {
// ...
});

個々の値でも、変更頻度が低い値に投影される場合があります。たとえば、ここでは、値自体ではなく、値の存在を示すブール値をコンポーネントが受け入れます。

function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}

const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});

メモ化されたコンポーネントに関数を渡す必要がある場合は、関数をコンポーネントの外側に宣言して変更されないようにするか、useCallback を使用して、再レンダリング間でその定義をキャッシュします。


カスタム比較関数の指定

まれに、メモ化されたコンポーネントの props の変更を最小限に抑えることが不可能な場合があります。その場合、浅い等価性を使用する代わりに、React が古い props と新しい props を比較するために使用するカスタム比較関数を提供できます。この関数は、memo の2番目の引数として渡されます。新しい props が古い props と同じ出力を生成する場合にのみ true を返す必要があります。それ以外の場合は false を返す必要があります。

const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}

これを行う場合は、ブラウザの開発者ツールの「パフォーマンス」パネルを使用して、比較関数が実際にコンポーネントの再レンダリングよりも高速であることを確認してください。驚くかもしれません。

パフォーマンス測定を行う際は、React が本番モードで実行されていることを確認してください。

落とし穴

カスタムの arePropsEqual 実装を提供する場合は、関数を含むすべての prop を比較する必要があります。関数は、多くの場合、親コンポーネントの props と state をクロージャとして保持します。oldProps.onClick !== newProps.onClick の場合に true を返す場合、コンポーネントは onClick ハンドラ内で以前のレンダリングからの props と state を引き続き「参照」し、非常に分かりにくいバグにつながります。

作業しているデータ構造が既知の限られた深さを持つことを100%確信していない限り、arePropsEqual 内で深い等価性チェックを実行しないでください。深い等価性チェックは非常に遅くなる可能性があり、後で誰かがデータ構造を変更した場合、アプリが数秒間フリーズする可能性があります。


トラブルシューティング

プロップがオブジェクト、配列、または関数の場合、コンポーネントは再レンダリングされます

Reactは、浅い等価性によって古いプロップと新しいプロップを比較します。つまり、各新しいプロップが古いプロップと参照的に等しいかどうかを考慮します。親コンポーネントが再レンダリングされるたびに新しいオブジェクトや配列を作成すると、個々の要素がそれぞれ同じであっても、Reactはそれを変更されたものと見なします。同様に、親コンポーネントをレンダリングするときに新しい関数を生成すると、関数の定義が同じであっても、Reactはそれが変更されたと見なします。これを回避するには、親コンポーネントでプロップを簡素化するか、メモ化します