Reactメモ化について

仕事でReactを使ってWebサイト構築をしています。Reactを使っている上でメモ化することが多々あるのですが、よくわからないまま使っています。プロジェクトメンバーにする必要あるの?これは有効なメモ化?と聞かれても答えられなかったので自分なりにまとめてみました。
※ 間違っている場合がありますので、ご自身で確認することをおすすめします。

React は v17.0.2 を使っています。

とりあえあうコードと一緒に勉強

下記の3つのファイルを用意してください。順番に App.tsx、CountDisplay.tsx、CountUpButton.tsxです。

import React, { useState } from 'react';
import CountDisplay from './CountDisplay';
import CountUpButton from './CountUpButton';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <CountDisplay count={count}/>
      <CountUpButton onClick={() => setCount(s => s + 1)}/>
    </div>
  );
};

export default App;
import React from 'react';

const CountDisplay = ({ count }: { count: number }) => {
  console.log(`CountDisplay: ${count}`);

  return (
    <p>カウント: {count}</p>
  );
};

export default CountDisplay;
import React from 'react';

const CountUpButton = ({ onClick }: { onClick: () => void }) => {
  console.log(`CountUpButton: ${onClick}`);

  return (
    <button onClick={onClick}>カウントアップ</button>
  );
};

export default CountUpButton;

これらを実行するとこんな感じになります。

画面ロード時に CountDisplay と CountUpButton が描画され、カウントアップのボタンを押す度に両方のコンポーネントが描画されています。
CountUpButton のコンポーネントが再描画されるのは無駄なので再描画されないようにします。

useCallback + React.memo を使ってみる

CountUpButton に渡している関数を useCallback を使ってメモ化した値を渡してみましょう

import React, { useState } from 'react';
import CountDisplay from './CountDisplay';
import CountUpButton from './CountUpButton';

const App = () => {
  const [count, setCount] = useState(0);

  const onClick = useCallback(() => setCount(s => s + 1), []);

  return (
    <div className="App">
      <CountDisplay count={count}/>
      <CountUpButton onClick={onClick}/>
    </div>
  );
};

export default App;

CountUpButton コンポーネントに渡される関数値は常に同じになりますが、これだけでは CountUpButton は再描画されてしまいます。
CountUpButton で React.memo をすることにより、CountUpButton の再描画を抑えることができます。

import React from 'react';

const CountUpButton = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log(`CountUpButton: ${onClick}`);

  return (
    <button onClick={onClick}>カウントアップ</button>
  );
});

export default CountUpButton;

React.memo でコンポーネントを作成することにより、propsの値が変更された場合のみ再描画されるようになります。

useMemo について

3回カウントアップしたことを数える state を用意します。新しく用意した state の値を用いて三角関数計算してそれを表示します。CountDisplay コンポーネントも React.memo を利用した形式に変更します。
(実際にこんな表示があったら意味がわからないですが。。。)

import React, { useCallback, useState } from 'react';
import CountDisplay from './CountDisplay';
import CountUpButton from './CountUpButton';

const calcResult = (second: number) => {
  console.log('calcResult');
  return Math.sin(second * 128) * Math.cos(second * -128);
};

const App = () => {
  const [count, setCount] = useState(0);
  const [second, setSecond] = useState(0);

  const onClick = useCallback(() => {
    setCount(s => {
      if ((s + 1) % 3 === 0) {
        setSecond(ss => ss + 1);
      }
      return s + 1;
    });
  }, []);

  const secondCalcResult = calcResult(second);

  return (
    <div className="App">
      <CountDisplay count={secondCalcResult}/>
      <CountDisplay count={count}/>
      <CountUpButton onClick={onClick}/>
    </div>
  );
};

export default App;
import React from 'react';

const CountDisplay = React.memo(({ count }: { count: number }) => {
  console.log(`CountDisplay: ${count}`);

  return (
    <p>カウント: {count}</p>
  );
});

export default CountDisplay;

CountDisplay にも React.memo を利用したので secondCalcResult を渡しているコンポーネント側は secondCalcResult の値が変更されたときのみ再描画されています。ですが secondCalcResult を計算するための関数は second の値が変わらなくとも実行されます。また count と second の state が両方変更した際には2回実行されています。

secondCalcResult の値をメモ化しましょう!second の値が変わって時のみ計算したいので useMemo の2つ目の引数の配列には second のみを指定します。

import React, { useCallback, useState } from 'react';
import CountDisplay from './CountDisplay';
import CountUpButton from './CountUpButton';

const calcResult = (second: number) => {
  console.log('calcResult');
  return Math.sin(second * 128) * Math.cos(second * -128);
};

const App = () => {
  const [count, setCount] = useState(0);
  const [second, setSecond] = useState(0);

  const onClick = useCallback(() => {
    setCount(s => {
      if ((s + 1) % 3 === 0) {
        setSecond(ss => ss + 1);
      }
      return s + 1;
    });
  }, []);

  const secondCalcResult = useMemo(() => calcResult(second), [second]);

  return (
    <div className="App">
      <CountDisplay count={secondCalcResult}/>
      <CountDisplay count={count}/>
      <CountUpButton onClick={onClick}/>
    </div>
  );
};

export default App;

second の値が変更されたときのみ secondCalcResult が再計算されるようになりました。

まとめ

React でのメモ化を見てきました。

個人的な考えですが React.memo はすべてのコンポーネントでやってもいいのかなと思います。

useMemo は描画内で実行されるので上手に使えば描画パフォーマンス向上につながりそうですね!

間違っていることや質問などがあれば質問箱からお待ちしております。