そろそろ無駄なレンダリング減らしたい頃かな(React)

公開:2020/10/23
更新:2020/10/23
5 min読了の目安(約5200字TECH技術記事

概要

Reactも書き始めて月日が立ち、なれてきた頃にそろそろ無駄なレンダリング減らしたいなーって思ったときに見ると役に立つかも

サンプルコード(改善前)

const Parent = () => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const handleSetCountA = () => {
    setCountA((prevCount) => prevCount + 1);
  };
  const handleSetCountB = () => {
    setCountB((prevCount) => prevCount + 1);
  };
  return (
    <div>
      <Button
        handleSetCountA={handleSetCountA}
        handleSetCountB={handleSetCountB}
      />
      <ChildA count={countA} />
      <ChildB count={countB} />
    </div>
  );
};

const ChildA = ({ count }) => {
  return <p>{count}</p>;
};

const ChildB = ({ count }) => {
  return <p>{count}</p>;
};

const Button = ({ handleSetCountA, handleSetCountB }) => {
  return (
    <div>
      <button onClick={handleSetCountA}>A</button>
      <button onClick={handleSetCountB}>B</button>
    </div>
  );
};

state(countA, countB)が更新されるとParentが再描画され、続いてButton, ChildA, ChildBも再描画されます。

今回改善したい無駄な再描画は更新前のpropsと更新後のpropsに変わりがないのにも関わらず再描画しているコンポーネント達です。

例えば、countBが更新されてcountAは更新されていないのにChildAは再描画しています。

改善の為に今回使用するのはReact.memouseCallbackです

React.memo()

公式ドキュメント引用

もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

第一引数に対象のコンポーネント、第二引数に比較関数を渡します。
第二引数はデフォルトでshallow equalで比較します。

ということなので、propsが変わらない時に再描画しないようにするには、対象のコンポーネントをmemoでくくってあげます。
以下サンプルコード修正版です。

※ 再描画されているか確認できるようにログを出しています。

const Parent = () => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const handleSetCountA = () => {
    setCountA((prevCount) => prevCount + 1);
  };
  const handleSetCountB = () => {
    setCountB((prevCount) => prevCount + 1);
  };
  return (
    <div>
      <Button
        handleSetCountA={handleSetCountA}
        handleSetCountB={handleSetCountB}
      />
      <ChildA count={countA} />
      <ChildB count={countB} />
    </div>
  );
};

const ChildA = React.memo(({ count }) => {
  console.log("ChildA render");
  return <p>{count}</p>;
});

const ChildB = React.memo(({ count }) => {
  console.log("ChildB render");
  return <p>{count}</p>;
});

const Button = React.memo((props) => {
  const { handleSetCountA, handleSetCountB } = props;
  console.log("button render");
  return (
    <div>
      <button onClick={handleSetCountA}>A</button>
      <button onClick={handleSetCountB}>B</button>
    </div>
  );
});

これで、countAが更新されたらChildAだけ再描画されて、ChildBは再描画されていません。

逆もしかりcountBが更新されたら、ChildBだけ更新されます。

あれ、なんでButtonが再描画されているの?と疑問を持つ方が出てきたと思います。

結論から言うと、第二引数に今回は何も渡していないため、shallow equalでpropsを比較しているからです。

shallow equalを簡単に説明します。
shallow equal
2つのオブジェクトのキーを1つづつ処理し、値を比較します。
値が同じならtrueを、違った場合はfalseを返す。

const prev = {
  a: 'hoge'
}

const next = {
  a: 'test'
}
shallowEqual(prev, next) // false

※注意点
値がオブジェクトや配列,アロー関数の場合は常に違うものと判断されてしまう。

const prev = {
  a: {
    b: 'hogehoge'
  }
}

const next = {
  a: {
    b: 'hogehoge'
  }
}
shallowEqual(prev, next) // false

今回Buttonに渡しているpropsがアロー関数なので、別物と扱われてしまい、Buttonが毎回再描画されてしまうということです。

これを解決するために使うのがuseCallbackです。

useCallback

公式ドキュメント引用

メモ化されたコールバックを返します。
インラインのコールバックとそれが依存している値の配列を渡してください。useCallback はそのコールバックをメモ化したものを返し、その関数は依存配列の要素のいずれかが変化した場合にのみ変化します。これは、不必要なレンダーを避けるために(例えば shouldComponentUpdate などを使って)参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利です。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

以下先程のコードからの修正版です。

import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";

const Parent = () => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const handleSetCountA = useCallback(() => {
    setCountA((prevCount) => prevCount + 1);
  }, []);
  const handleSetCountB = useCallback(() => {
    setCountB((prevCount) => prevCount + 1);
  }, []);
  return (
    <div>
      <Button
        handleSetCountA={handleSetCountA}
        handleSetCountB={handleSetCountB}
      />
      <ChildA count={countA} />
      <ChildB count={countB} />
    </div>
  );
};

const ChildA = React.memo(({ count }) => {
  console.log("ChildA render");
  return <p>{count}</p>;
});

const ChildB = React.memo(({ count }) => {
  console.log("ChildB render");
  return <p>{count}</p>;
});

const Button = React.memo((props) => {
  const { handleSetCountA, handleSetCountB } = props;
  console.log("button render");
  return (
    <div>
      <button onClick={handleSetCountA}>A</button>
      <button onClick={handleSetCountB}>B</button>
    </div>
  );
});

これでどちらのcountを更新してもButtonは再描画されなくなりました。

countA更新

countB更新