React.memo と useCallbackで state の変化に伴う{個別,共通}コンポーネントの再描画を抑制する
Tags: typescript, react
Published: 2022-05-23 07:27:41


## why

パフォーマンスチューニングのやり方を整理したかった

## 参考動画

https://www.youtube.com/watch?v=KXhE1tBBfJc

あべちゃんさんの React Hooks の動画

【ReactHooks入門】第6回：useCallbackの理解

を参考にしました。

---

## そもそもなぜ React の　useState は値が変わって再描画されるのか

https://zenn.dev/taroro28/articles/3bec0f3f4711e9#%E4%BD%99%E8%AB%87%3A-%E5%90%8C%E3%81%98%E5%80%A4%E3%81%A7setstate()%E3%81%97%E3%81%9F%E3%82%89%E5%86%8Drender%E3%81%95%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F

taroro28 さんの Zenn のこの記事に答えがあった

```php
react/packages/react-reconciler/src/ReactFiberHooks.new.js
```

react ライブラリのここで setState が行われたときに
eagerState, currentState の比較が行われる
それで違う場合に再描画が起こるらしい。

---

## CRA

```php
npx create-react-app pf --template typescript
```

pf という名前で　CRA

---

## Title コンポーネントを作成

https://www.youtube.com/watch?v=KXhE1tBBfJc&t=510s

```ts
type Props = { titleText: string }
const Title: React.FC<Props> = ({titleText}) => {
  return (
      <h2> {titleText} </h2>
  );
}
export default Title;
```

App から Props として受け取る titleText を
そのまま Props という型で定義して

コンポーネントに React.FC にはめて titleText を引数処理して
h2 でラップして 返す処理を書いた。

---

## App で titleText を渡して呼び出す

```ts
import Title from './components/Title'
function App() {
  const titleText = '#6 useCallback'
  return (
    <div className="App">
      <Title titleText={titleText} />
    </div>
  );
}
export default App;
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/usnp5tnix5tc0eev87sl.png)
 
これで読み込めた。

Subtitle も同じように作る

---

## A と B のカウンターを div で追加

これらに console.log を仕込んでも最初は一度ずつしか読み込まれない。
useState とか一切ないから。

しかし、ここに状態を持ち込んで変化させる関数を useState で導入すると
問題が表明化する。

```ts
function App() {
  const [countA, setCountA] = useState<number>(0)
  const [countB, setCountB] = useState<number>(0)

  const titleText = '#6 useCallback'
  const subTitleText = 'アンケート'

  return (
    <div className="App">
      <Title titleText={titleText} />
      <SubTitle subTitleText={subTitleText} />
      <div>{countA}</div>
      <div>{countB}</div>
      <button onClick={ () => setCountA(countA+1)}>A に 1 票</button>
      <button onClick={ () => setCountB(countB+1)}>B に 1 票</button>
    </div>
  );
}
export default App;
```

countA と countB の状態を 0 で作って
押すと加算される ボタンをそれぞれ作る。

---

## Title, SubTitle に console.log を追加する

```ts
type Props = { titleText: string }
const Title: React.FC<Props> = ({titleText}) => {
  console.log('Title rendered');
  
  return (
      <h2> {titleText} </h2>
  );
}
export default Title;
```

Title と SubTitle の内部で console.log を呼ぶようにしておく

---

## countA, countB が動いた時に Title, SubTitle まで再度読み込まれているところを確認る

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oa36k32020qnocqwum2d.png)

そうすると、countA, countB が変更して再レンダリングされたタイミングで
全く関係ない Title, SubTitle コンポーネントまで再度読み込まれてしまっているのがわかる。

これを useMemo を使って改善していく。

---

## Title コンポーネントで React.memo を引数に追加して中身を () で囲う。

```ts
const Title: React.FC<Props> = React.memo(({titleText}) => {
  console.log('Title rendered');
  return (
      <h2> {titleText} </h2>
  );
})
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zat2b79d6krw0fv7oq76.png)

すると　count の変化で Title コンポーネントが変化しなくなる。

一方、Button を共通コンポーネント化すると
countA が動いた時でも ButtonA だけでなく
ButtonB まで動いてしまう問題が残っている。

これを

--- 

## Counter コンポーネントを作成して使って countA, countB を表示するようにする

```ts
import React from 'react'
type Props = { 
  counterTitle: string;
  count: number;
}

const Counter: React.FC<Props> = React.memo(({counterTitle, count}) => {
  console.log(`Counter: ${counterTitle} rendered`);
  
  return (
      <div> {counterTitle}: <span>{count}人</span></div>
  );
})
export default Counter;
```

counterTitle と count を受け取って表示するコンポーネントを作って

```ts
      <Counter counterTitle={'A'} count={countA} />
      <Counter counterTitle={'B'} count={countB} />
```

App で呼び出す

---

## Button コンポーネントを共通化して onClick と buttonText を受け取って {A,B} に一票を動かせるようにする

```ts
import React from 'react'
type Props = {
  buttonText: string;
  onClick: () => void;
};

const Button: React.FC<Props> = React.memo(({ buttonText, onClick }) => {
  console.log(`Button:${buttonText} rendered`);

  return (
    <div >
      <button onClick={onClick} type='button' >
        {buttonText}
      </button>
    </div>
  );
});

export default 
```

buttonText と onClick を受けとる
Button コンポーネントを作って

```ts
      <Button onClick={handleCountUpA} buttonText='A に 1 票' />
      <Button onClick={handleCountUpB} buttonText='B に 1 票' />
```

A に一票追加
B に一票追加

これらを App でこのコンポーネントで動かす。

---

## handleCountUp{A,B} に useCallback を count{A,B} を引数で組み込んで動作時に App 全体を読み込まれないようにする

```ts
  const handleCountUpA = () => {
    setCountA(countA + 1)
  }
```

この handleCountUpA を

```ts
  const handleCountUpA = useCallback(() => {
    setCountA(countA + 1)
  }, [countA])
```

useMemo と同じように 引数から関数が閉じるまでの
`() => {}` の部分を useCallback() でくくる。

useEffect と同じように対象の変数を指定する(必須)

---

## useCallBack

useCallBack なしで B に一票ボタンをクリックすると

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rcqah6in11kjz5tg0q3n.png)
 
A ボタンまで再度読み込まれてしまう。

ここにさっきの useCallback を追加して
B ボタンを押すと

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8bxqdh4tc03w7yftcag4.png)

今度は B ボタンのみが再描画された。

---

## まとめ

通常は App の useState で実装された状態変数が変更されると
中の全てのコンポーネントが再描画されてしまう。
これは計算の無駄であり、パフォーマンスの低下につながる。

```ts
const Title: React.FC<Props> = React.memo(({titleText}) => {
  console.log('Title rendered');
  
  return (
      <h2> {titleText} </h2>
  );
})
```

全く別のコンポーネントであれば
このように React.memo() で引数から関数の終わりまで括ると
引数に変化がないときは、関係ない状態変数が変化しても
再描画されないようになる。

Button のように複数のコンポーネントで関数を渡して使われている汎用コンポーネントでは、React.memo() を使っても、どれかの Button が使われるたびに全ての Button が再描画されてしまう。

そこで Button の onClick に仕込むハンドル関数自体に

```ts
  const handleCountUpA = useCallback(() => {
    setCountA(countA + 1)
  }, [countA])
```

このように useCallback() で括って引数に特定の状態変数を取れば
その状態引数を使ったコンポーネントのみ描画されるようになる。

countA, countB の状態があってそれぞれ
ButtonA, ButtonB があると
countA の変化で ButtonA のみが再描画されるようになる。