React：memoとuseCallbackでリストのパフォーマンス向上


## 問題

![スクリーンショット 2025-01-24 0.53.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576061/dd2501e7-f19e-6574-b45e-ccc8db156f0b.png)

上記のようなコンポーネントを作りました。
CardListコンポーネントの中でCardListItemコンポーネントが繰り返されている形です。

CardListコンポーネントではitemValuesという状態を持ちます。
もしどれかのカードの入力欄に値を入れると、親でitemValuesが更新され、全てのカードが再描画されてしまいます。

それでは、入力が実行されたカードだけ再描画できるようにしていきましょう。


## リファクタ前のコード
CardList.tsx↓
```tsx
import React, { useState } from "react";
import CardListItem from "./CardListItem";

const CardList = () => {
  const [itemValues, setItemValues] = useState([
    { id: 1, value: "" },
    { id: 2, value: "" },
    { id: 3, value: "" },
  ]);

  const changeValue = (id: number, value: string) => {
    setItemValues(
      itemValues.map((item) => {
        if (item.id === id) {
          item.value = value;
        }
        return item;
      })
    );
  };

  return (
    <div>
      <div className="mb-3">itemValues: {JSON.stringify(itemValues)}</div>
      <div className="w-[50%] flex gap-2">
        {itemValues.map((item) => (
          <CardListItem
            key={item.id}
            id={item.id}
            value={item.value}
            onChange={changeValue}
          />
        ))}
      </div>
    </div>
  );
};

export default CardList;

```
CardListItem.tsx↓
```tsx
type CardListItemProps = {
  id: number,
  value: string,
  onChange: (id: number, value: string) => void,
};

const CardListItem = ({ id, value, onChange }: CardListItemProps) => {
  return (
    <div className="w-[50%] flex flex-col mb-2 rounded-lg bg-lime-300 p-4">
      <label htmlFor={id.toString()}>id: {id}</label>
      <input
        id={id.toString()}
        type="text"
        value={value}
        onChange={(e) => onChange(id, e.target.value)}
      />
    </div>
  );
};

export default CardListItem;

```

## リファクタ後のコード

### 変更１：CardListItemコンポーネントをmemo化する

memo化とは再計算を防ぐためにキャッシュ化することで、
メモ化されたコンポーネントは、props が変化しない限り再レンダリングされません。

```tsx
import { memo } from "react";

type CardListItemProps = {
  id: number,
  value: string,
  onChange: (id: number, value: string) => void,
};

const CardListItem = ({ id, value, onChange }: CardListItemProps) => {
  return (
    <div className="w-[50%] flex flex-col mb-2 rounded-lg bg-lime-300 p-4">
      <label htmlFor={id.toString()}>id: {id}</label>
      <input
        id={id.toString()}
        type="text"
        value={value}
        onChange={(e) => onChange(id, e.target.value)}
      />
    </div>
  );
};

// memo化
export default memo(CardListItem);

```

これで入力値が変わったCardListItemしか変更しないはずですよね、、
でも実はCardListItemコンポーネントに変更を加えると、
CardListが再描画され、その中で定義しているchangeValue関数も新しく作成され、異なる参照として扱われます。
つまりCardListItemコンポーネントに渡されるpropsのonChangeが変更したこととなり、せっかくmemo化したCardListItemコンポーネントも再描画されてしまいます。

### 変更２：useCallbackで関数の再描画を防ぐ

`useCallback` を使うと、依存配列が変化しない限り、同じ関数の参照を再利用します。
CardListItemに渡すonChangeが変わらなくなるということですね。
これでmemoが活きます。

```tsx
import React, { useCallback, useState } from "react";
import CardListItem from "./CardListItem";

const CardList = () => {
  const [itemValues, setItemValues] = useState([
    { id: 1, value: "" },
    { id: 2, value: "" },
    { id: 3, value: "" },
  ]);

  // useCallback関数内に入れる
  const changeValue = useCallback((id: number, value: string) => {
    setItemValues(
      itemValues.map((item) => {
        if (item.id === id) {
          item.value = value;
        }
        return item;
      })
    );
  }, []);

  return (
    <div>
      <div className="mb-3">itemValues: {JSON.stringify(itemValues)}</div>
      <div className="w-[50%] flex gap-2">
        {itemValues.map((item) => (
          <CardListItem
            key={item.id}
            id={item.id}
            value={item.value}
            onChange={changeValue}
          />
        ))}
      </div>
    </div>
  );
};

export default CardList;

```

## まとめ

- 繰り返されるコンポーネントはmemo化することを検討しましょう
- 関数を子供に渡している場合、その関数が不必要に参照が変わらないようuseCallbackで囲みましょう