memo化したのに再レンダリングされてしまった話 – React.memo

経緯

フリーランスになってから早2年。前のプロジェクトが解散となり、新しくReact JSのプロジェクトに参画しました。Reactに関してはほぼ初めてでしたが、最近慣れてきたので、ぼちぼちReactに関する記事も書いていこうかと思います。

React.memo を使ったときにうまく働いていなさそうだったので、調べたことをまとめました。

React.memo とは?

通常、Reactのコンポーネントは親が再レンダリングされると、子も同じく再レンダリングされます。

再レンダリング前後で、子コンポーネントに同じプロパティ(props)が渡されるとき、当然、子コンポーネントは同じものになるため、再レンダリングするのはただの無駄です。

React.memoを用いると、コンポーネントが再レンダリング前と同じプロパティ(props)を受け取ったとき、再レンダリングをスキップできます。

その結果、たくさんのデータを扱ったり、頻繁に画面が更新される場合に、画面描画のパフォーマンスが良くなります。

基本的な使い方

React.memoの使い方はいたってシンプルで、関数コンポーネントをラップするだけです。

例えば、以下のようなコードにしておけば、親が再レンダリングされてもコンポーネントに渡されるprops.valueが変わらない限り、再レンダリングされません。

import React from 'react';

const MyComponent = React.memo((props) => {
  return (<div>{props.value}</div>);
});

export default MyComponent;

再レンダリングをスキップする例

以下の例では、親コンポーネントの「Increment Parent」ボタンをクリックすると親は再レンダリングされますが、React.memoでラップされたChildComponentは、childCountが変わらない限り、表示が変わることがないため再レンダリングされません。

import React, { useState } from 'react';
const ChildComponent = React.memo(({ count }) => {
  return (<div>Child Count: {count}</div>);
});

const ParentComponent = () => {
  const [parentCount, setParentCount] = useState(0);
  const [childCount, setChildCount] = useState(0);
  return ( 
    <div>
      <div>Parent Count: {parentCount}</div>
      <button onClick={() => setParentCount(parentCount + 1)}>
        Increment Parent
      </button>
      <ChildComponent count={childCount} />
      <button onClick={() => setChildCount(childCount + 1)}>
        Increment Child
      </button>
    </div>
   );
};
export default ParentComponent;

このコードでは、親コンポーネントの「Increment Parent」ボタンを押しても、ChildComponentは再レンダリングされません。子コンポーネントのカウントが変わらない限り、表示が変わらないためです。

注意点

これだけ聞くと、全部React.memoでラップすればいいじゃないか!と思いがちですが、いくつか注意するべき点があります。

1. propsの比較がうまくいかないことがある

プロパティにオブジェクトや配列が渡された場合、中身が同じであっても、新しいものと認識され、再レンダリングされてしまうことがあります。

React.memoでは、第二引数に比較関数を与えることで、正しく判定することができるようになります。

const MyComponent = React.memo(
  (props) => {
    return <div>{props.values.join(',')}</div>;
  },
  (prevProps, nextProps) => {
    return prevProps.values.join(',') === nextProps.values.join(',');
  }
);

上記のコードでは場合、valuesが配列ですが、一度、文字列に変換して中身を比較することで、配列の中身が変わらない限り再レンダリングを防いでいます。

2. すべてのケースでパフォーマンス向上するわけではない

当然ですが、再レンダリングすることがないのに、React.memoを使ってしまうと逆にパフォーマンスが下がってしまいます。

コンポーネントが超シンプルだったり、再レンダリングすることが少ない場合は、逆にメモ化の処理が負担になることもあるので、本当に必要か考えてから使ったほうがいいと思います。

(ただ、memo化の処理がめちゃめちゃ負荷になることは少ないので、めっちゃ大きいプロジェクトじゃない限り、全部memo化してもいいんじゃね?とか思ってます)

まとめ

memoは、不要な再レンダリングを防げて便利ですが、注意しないとうまく動かないことがあります。

オブジェクトや配列のプロパティは、うまく判定できないことがありますが、必要に応じてカスタムの比較関数を使うと、柔軟に再レンダリングのタイミングを制御できます。

propsに配列やオブジェクトがある場合、疑ってみてもいいかもしれません。