🥝

React.memoの特徴と使い方

2023/06/11に公開

背景

reactのパフォーマンスチューニングについて調べた時、React.memoの特徴と使い方を深く学習することができたので、備忘録としてまとめておきます。

React.memoについて学習している方の参考になれば幸いです。

React.memoとは?

React.memoは、コンポーネントの props が変更された場合のみ再レンダリングを行うためのものです。

props が変更されない限り、前回のレンダリング結果を再利用します。つまり、React.memoを使用することで コンポーネントに渡る props が変更されていない場合、そのコンポーネントの再レンダリングをスキップすることができます。

ドキュメント
https://react.dev/reference/react/memo

 
React.memoを使用するときは、下記のような書き方をします。

import { memo } from 'react’

const Count = memo(( props ){ }); 

props以外の値が渡ってきても再レンダリングされないが、propsの値が渡ってきてその値が変更されたら再レンダリングされます。

また、親componentのstateが更新されても、React.memoを使用した子componentに渡されるpropsが変更・更新されない限り、再レンダリングされません。

具体的な使用例は以下の通りです。

import React from 'react';

function MyComponent({ value }) {
  return <div>{value}</div>;
}

// propsが変更されない限り再レンダリングをスキップする
const MemoizedComponent = React.memo(MyComponent);

function ParentComponent() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MemoizedComponent value={count} />
    </div>
  );
}

上記の例では、MyComponent コンポーネントを React.memo でラップし、React.memo によって MemoizedComponentvalue の値が変更されない限り再レンダリングをスキップしています。

これにより、ParentComponent の再レンダリング時にも props のvalue に変化がなければ、MemoizedComponent が再計算されずに前回の結果を再利用することができます。

ただし、React.memo を使用する際にはいくつかの注意点があります。

React.memo は shallow compare (参照先かどうかの比較)を行いますが、複雑なオブジェクトや配列の場合は変更を検出できない可能性があります。この場合は、カスタム比較関数を指定することで 対応することができます。

React.memo は再レンダリングの最適化に貢献しますが、必ずしもすべてのコンポーネントで使用する必要はありません。単純なコンポーネントや再レンダリングのコストが低い場合は、React.memo を適用しなくても十分なパフォーマンスを実現できる場合もあります。

 

React.memoのpropsの比較について

React.memoでは、props が変更された場合のみ再レンダリングを行います。

つまり、React.memoでは、再レンダリングする必要があるかどうかを判断するために、前回のpropsの値と現在のpropsの値の比較を行います。

Reactでは等価性の比較を Object.is を用いて行なっており、React.memo の props の比較やuseEffect や useCallback, useMemo の依存配列の比較などにも Object.is が使用されています。

ただし、オブジェクトや配列、関数などのオブジェクト型(プリミティブ型ではないもの)を比較する場合、値そのものではなく参照先データを比較します。

参考

https://ja.javascript.info/object-copy

https://nishinatoshiharu.com/js-shallow-deep/

つまり、同じキーと値を持つオブジェクトや同じ値を持つ配列、同じコードで書かれた関数でも、再レンダリング前後では異なるものとして認識されてしまいます。

したがって、React.memoを使用したコンポーネントにオブジェクト型のpropsを渡すと、propsの変更を検知し、再レンダリングが発生します。

参考記事

https://weseek.co.jp/tech/3917/

https://zenn.dev/bom_shibuya/articles/b07da034fc5686

https://numb86-tech.hatenablog.com/entry/2019/12/20/222412

https://tech.stmn.co.jp/entry/2021/02/22/140028

 

カスタム比較関数

メモ化されたコンポーネントの props の変更を最小限にすることが困難な場合に、カスタム比較関数(前回のpropsと新しいpropsを受け取り、それらを比較して同じかどうか判断する関数)を使用することができます。

React.memoの第二引数にこの関数を設定することでこの関数を使用することができ、古い props と新しい props を比較し、同じ出力結果をもたらす場合には再レンダリングを抑制することができます。

下記がカスタム比較関数の使用例となります。

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;
    })
  );
}

上記のコードは、Chart というコンポーネントを React.memo でメモ化しています。メモ化されたコンポーネントは、dataPoints という props が変更された場合にのみ再レンダリングされます。

arePropsEqual というカスタム比較関数が定義されており、これが Chart コンポーネントの再レンダリングの条件を制御しています。この関数は古い props と新しい props を受け取り、それらが同じ出力結果をもたらすかどうかを判定しています。

具体的には、arePropsEqual 関数では、dataPoints の配列の長さが同じであり、各要素の xy の値が全て一致しているかどうかをチェックしています。配列の要素ごとに比較を行い、すべての要素が一致している場合にのみ再レンダリングを抑制します。

このように、arePropsEqual 関数を使用することで、Chart コンポーネントは dataPoints の中身が変更されない限り再レンダリングされません。つまり、dataPoints の要素が同じであれば、コンポーネントの再レンダリングが抑制されます。これにより、不要な再レンダリングを避け、パフォーマンスを向上させることができます。

参考

https://coders-shelf.com/react-memo/

https://numb86-tech.hatenablog.com/entry/2019/12/20/222412

 

関数を props として渡しているとき

関数をpropsとして渡すときは、再レンダリングされるたびに props に変更があったとみなされ、関数を再生成するので、React.memoでは再レンダリングを防ぐことはできません。

この場合、useCallbackを使用することで不要な再レンダリングを防ぐことができます。

参考

https://react.dev/reference/react/useCallback

 

まとめ

以上が公式ドキュメントをもとに自分なりにまとめたReact.memoの特徴と使い方になります。

今後は、React.memoをうまく活用し、reactアプリケーションのパフォーマンスチューニングを行なっていきたいです。

Discussion