基礎から学ぶReact/React Hooks学習メモ 5-3 useCallback React.memo useMemo

# React Hooksを基礎から理解する 5-3 useCallback React.memo useMemo

参考
- [基礎から学ぶReact/React Hooks](https://www.amazon.co.jp/dp/B09CP8T4TX)

- useCallback()、useMemo()は、パフォーマンスチューニングのためのフック
- コンポーネントの不要な再レンダリングを防ぐ
- Reactコンポーネントでは、state、propsが更新されるたびに再レンダリングが行われる
- useCallback()、React.memo()、useMemo()は、前回の差分がないことが事前に分かっている場合、再レンダリングをスキップできる
- useCallback()は、React.memo()と組み合わせて使用する
- メモ化とは
    - 同じ結果を返す処理について、初回のみ処理を実行して記録
    - 2回目以降は、保持していた結果を再利用する

## React.memoとは

- コンポーネントのレンダリング結果をメモ化するReact API
- 親コンポーネントから渡されるpropsについて、変更差分をチェックして、コンポーネントが返した描画結果を記録してメモ化し、差分があった倍の三再レンダリングする
    - propsの前後を比較し、等価かどうかチェック
    - 等価であれば、メモ化したコンポーネントを利用するので、再レンダリングをスキップ
    - 等価でなければ、再レンダリングする
- React.memo()でラップしていても、コンポーネント内部でuseState()やuseContext()を使用している場合、その変化に応じた再レンダリングが発生する
- useMemo()の基本構文

```js
React.memo(親コンポーネントからpropsを受け取る子コンポーネント)
```

- React.memo()の利用例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/e74a1a0c-0a73-9120-b55f-bc61773b53d0.png)

- React.memo()未使用時（AボタンとBボタンを1回ずつ押す）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/fa880320-80c3-1961-9bcf-7fae2d9eda98.png)


- React.memo()使用時（AボタンとBボタンを1回ずつ押す）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/77714866-7aaa-29bd-d49f-c2daf7679320.png)


```js
import React, { useState } from "react";
import "./styles.css";

// React.memo()で関数全体を括る。propsが同じときはメモ化したコンポーネントを使用する
const CountResult = React.memo(({ text, countState }) => {
  console.log(`${text}ボタンがクリックされました！`);
  return (
    <p>
      {text}: {countState}
    </p>
  );
});

const Counter = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  const countIncrementA = () => {
    setCountStateA((prevCount) => prevCount + 1);
  };
  const countIncrementB = () => {
    setCountStateB((prevCount) => prevCount + 1);
  };

  return (
    <>
      <CountResult text="Aボタン" countState={countStateA} />
      <CountResult text="Bボタン" countState={countStateB} />

      <button onClick={countIncrementA}>Aボタン</button>
      <button onClick={countIncrementB}>Bボタン</button>
    </>
  );
};

export default function App() {
  return <Counter />;
}
```

## useCallbackとは

- React.memoを使っても親コンポーネントから、「コールバック関数」をpropsとして受け取った場合、子コンポーネントは再レンダリングされてしまう。
- 関数自体のメモ化に利用できるのが、メモ化されたコールバック関数を返すuseCallback()
- インポート

```js
import React, { useCallback } from "react";
```

- useCallback()の基本構文

```js
useCallback(コールバック関数, [コールバック関数が依存している要素の配列]);
```

- a,bどちらも変化しなければ、メモ化した関数を再利用する

```js
// メモ化したいsampleCallbackFunc関数を宣言
const sampleCallbackFunc = () => doSomething(a, b);
// useCallbackでメモ化したい、sampleCallbackFunc関数をラップ
const memorizedSampleCallbackFunc = useCallback(sampleCallbackFunc, [a, b]);
```

- 配列を空にすると、初回レンダリング時に生成した関数をずっと使い続ける

```js
const memorizedSampleCallbackFunc = useCallback(sampleCallbackFunc, []);
```

- useCallbackの利用例
    - useCallback()は、React.memo()と組み合わせて使用する
    - useCallback()で使用する配列の項目は、関数内で使われてないといけない。逆に使われていないときはその項目はいらない。
    - `setCountStateA(countStateA + 1)` を、`setCountStateA((prevCount) => prevCount + 1)` とすれば、`[]`で良い

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/5e30f625-0a3e-6d5b-44b5-4f2eeb543f2c.png)

- Aボタン、Bボタンを1回ずつ押したとき、異なるボタンのログが出ていない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/d8e96eb0-00a4-fcde-9f2d-f81aa891f13e.png)


```js
import React, { useState, useCallback } from "react";
import "./styles.css";

// useCallback()は、React.memo()と組み合わせて使用する
const Button = React.memo(({ counterState, buttonValue }) => {
  console.log(`${buttonValue}がクリックされました！`);
  return <button onClick={counterState}>{buttonValue}</button>;
});

const Counter = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  // 関数をメモ化する
  const countIncrementA = useCallback(() => {
    setCountStateA(countStateA + 1);
  }, [countStateA]);
  // 関数をメモ化する
  const countIncrementB = useCallback(() => {
    setCountStateB(countStateB + 1);
  }, [countStateB]);

  return (
    <>
      <p>Aボタン: {countStateA}</p>
      <p>Bボタン: {countStateB}</p>
      <Button counterState={countIncrementA} buttonValue="Aボタン" />
      <Button counterState={countIncrementB} buttonValue="Bボタン" />
    </>
  );
};

export default function App() {
  return <Counter />;
}
```

## useMemo

- 関数の結果を保持する
- 何度計算しても結果が同じ場合の、「関数による計算の結果」をメモ化し、そこから値を取得する
- useCallback() → 関数自体をメモ化する
- useMemo() → 関数の結果を保持する
- インポート

```js
import React, { useMemo } from "react";
```

- useMemo()の基本構文

```js
useMemo(() => 値を計算する関数の呼び出し, [値の計算に必要な要素の配列]);
```

- a,b のどちらかに変更があった場合、関数を再実行する。

```js
useMemo(() => doSomething(a, b), [a, b]);
```

- 第二引数を空とした場合、初回の一度のみ実行され、それ以降はキャッシュから値を取得する

```js
useMemo(() => doSomething(a, b), []);
```

- useMemoの利用例
    - Bの計算がとても重いプログラム

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/6df692cf-6001-b531-9499-541bece33f48.png)


- useMemo()を使用したとき（A,Bボタンを1回ずつクリック。重い処理が1度実行）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/06dc8fe3-290a-6820-28fe-51e0da2e5eda.png)


- useMemo()を使用しないとき（A,Bボタンを1回ずつクリック。重い処理が2度実行）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/80e50cf4-e696-3641-eb82-b8082d8d87de.png)


```js
import React, { useState, useMemo } from "react";
import "./styles.css";

const square = (param) => {
  const testData = [...Array(1000).keys()];

  testData.forEach(() => {
    console.log(
      `「計算: B + 1」がボタンクリックされ、square関数実行、ループ処理を${testData.length}回実行中...`
    );
  });
  return param * param;
};

const Counter = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  // squreがとても重い処理なので、countStateBが変化した時だけ実行してほしい
  const squareArea = useMemo(() => square(countStateB), [countStateB]);
  // ラップしないとき
  // const squareArea = square(countStateB);

  const countIncrementA = () => {
    setCountStateA((prevCount) => prevCount + 1);
    console.log("計算: A + 1ボタンがクリックされました！");
  };
  const countIncrementB = () => {
    setCountStateB((prevCount) => prevCount + 1);
    console.log("計算: B + 1ボタンがクリックされました！");
  };

  return (
    <>
      <p>
        計算結果A: {countStateA}
        <button onClick={countIncrementA}>計算: A + 1</button>
      </p>
      <p>
        計算結果B: {countStateB}
        <button onClick={countIncrementB}>計算: B + 1</button>
      </p>
      <p>【正方形の面積】</p>
      <p>計算結果B × 計算結果B = {squareArea}</p>
    </>
  );
};

export default function App() {
  return <Counter />;
}
```