【React】useMemoとuseCallbackの違いを理解してパフォーマンス最適化する

# 疑問

Reactでパフォーマンス最適化を行う際に「`useMemo`」と「`useCallback`」というものが登場するが、これらの違いって何...？

# 結論

Reactでは、「特定の場合にしか必要のない処理なのに、コンポーネントの再レンダリングによって無駄にその処理が再実行されてしまう」ということがたまに発生します。

そこでReactでは、「この条件を満たす場合に **だけ** 処理を再実行せい」という指定をすることができます。これがいわゆる「**メモ化**」と呼ばれるものです。

`useMemo`と`useCallback`はこの「メモ化」を行うための手段で、「何に対して再実行の条件をつけるか」という点が異なります。

⭐️ `useMemo`は、「**処理の再計算**」に条件をつけることができます。


⭐️ `useCallback`は、「**関数の再定義**」に条件をつけることができます。


# 詳しく解説

Reactコンポーネントではリアクティブなデータを定義する際に`useState`を使用します。そのデータが更新されると、コンポーネントの再レンダリングが実行されます。

以下のようなコンポーネントを例とすると、テキストフォームの入力値でStateの`name`を更新しているので、フォームへの入力がある度にブラウザのコンソールに「再レンダリング」と出力されます。

この挙動は時に画面が重くなる原因となります。

```react
export const HelloWorld = () => {
  const [name, setName] = useState<string>(""); // Stateを定義

  console.log("再レンダリング"); // nameが更新される度にログが出力される

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)} // 入力値でStateの値nameを更新
        value={name}
      />
    </div>
  );
};
```

## useMemoとは何か / 使い所

以下のようなコンポーネントがあるとします。とても重い処理を内部で行なっている関数 `heavyCalc` の結果を `doubleCount` という変数に格納しています。

この`heavyCalc`関数内で使用している（依存している）Stateの値は`count`だけなので、`count`が更新された場合のみ「`const doubleCount = heavyCalc();`」が再実行されて欲しいのですが、

先述の例でお話した通り、ReactではStateの値に更新があった場合にはコンポーネント内の処理が再実行される為、フォームに値が入力されて`heavyCalc`関数に全く関係のないState `name` が更新された場合にも、毎回「`const doubleCount = heavyCalc();`」が再実行されてしまいます。

```react

export const HelloWorld = () => {
  const [name, setName] = useState<string>("");
  const [count, setCount] = useState<number>(0);

  const heavyCalc = (): number => {
    let x = 0;

    for (let i = 0; i < 100000000; i++) {
      x++;
    }

    return count * 2;
  };

  const doubleCount = heavyCalc(); // レンダリングされる度に実行される

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      /><br/><br/>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <p>Double count: {doubleCount}</p>
    </div>
  );
};
```
その結果、テキストフォームに値を入力するだけでも処理時間が長くなり、かなり画面がもっさりしてしまいます。

![Screenshot 2024-07-25 at 8.54.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/8e062bb0-973b-c753-e7ee-25a37265f989.png)


そこで使用するのが「`useMemo`」です。

`useMemo`は、「**処理の再計算**」に条件をつけることができます。

`heavyCalc();` という処理の再計算に条件をつけることでパフォーマンス最適化を図ることができます。

使い方は簡単です。第一引数に「再計算されてほしくない処理」を指定して、第二引数に「コイツが更新されたら再計算していいよ。なヤツ」を指定します。

再計算されてほしくない処理は `heavyCalc();` で、その処理が依存する値（コイツが更新されたら再計算していいよ。なヤツ）は `count` なので、`useMemo(() => heavyCalc(), [count])`のような形になります。

```react
export const HelloWorld = () => {
  const [name, setName] = useState<string>("");
  const [count, setCount] = useState<number>(0);

  const heavyCalc = (): number => {
    let x = 0;

    for (let i = 0; i < 100000000; i++) {
      x++;
    }

    return count * 2;
  };

  const doubleCount = useMemo(() => heavyCalc(), [count]); // countが更新された時だけ再計算

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <p>Double count: {doubleCount}</p>
    </div>
  );
};
```

そうすると、以下のようにフォームに文字列を入力した際の処理速度が 122.3ms から 0.2ms と、かなり早くなりました。画面のもっさり感は跡形もなくなったと思います！👏

`count` が更新された際には、適切に `heavyCalc()` が再計算されていることも確認できると思います。

![Screenshot 2024-07-25 at 8.53.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/348702c8-a285-e417-3926-5278e7b0a20e.png)


このように、`useMemo` は処理の再計算に条件をつけることができます。


## useCallbackとは何か / 使い所

`useCallback`を理解する際には、React内のDOMツリーにおけるルールについて理解しておくとスムーズに理解が進みます。

Reactでは、Stateの値の更新などによって再レンダリングが実行された場合、その子コンポーネントにあたるものも、再レンダリングが実行されます。

これが時に無駄な挙動となりうるので、コンポーネント自体をメモ化することでこれを回避することができます。

![Screenshot 2024-07-25 at 8.40.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/84f44c78-421f-dc25-2dfd-d6b7be17d8d0.png)

```react
const Child = () => {
  return <p>子コンポーネント</p>;
};

export const Parent = () => {
  const [name, setName] = useState<string>("");

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <Child />
    </div>
  );
};
```
以下のように、テキストフォームに入力によってState `name` が更新された際、`Child`コンポーネントも再レンダリングが行われていることが分かります。

![Screenshot 2024-07-25 at 8.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/7954cdaf-044e-76aa-d4c4-1988235e01ac.png)

ここで使用するのが、「**コンポーネントのメモ化**」です。

Reactで提供されている`memo`を使用することでコンポーネントをメモ化することができます。

`Child`コンポーネントを`memo`で囲ってあげるだけで完了です。

```react
const Child = React.memo(() => {
  return <p>子コンポーネント</p>;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <Child />
    </div>
  );
};
```
そうすると、テキストフォームに入力によってState `name` が更新されたとしても、`Child`コンポーネントも再レンダリングは行われず、`Parent`のみ適切に再レンダリングが実行されていることが分かります。

![Screenshot 2024-07-25 at 8.59.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/605d80f1-5349-e5cb-5592-724f0cd0d556.png)

今回の場合、`Child`がとてもシンプルなコンポーネントだった為、メモ化を行わなくても目に見えてパフォーマンスが落ちるということはありませんが、たとえば`Child`コンポーネントに変更してあげると、メモ化の有無によってパフォーマンスに大きく違いが現れます。

```react
const Child = () => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }

  return <p>子コンポーネント</p>;
};
```

**では本題の、`useCallback`はいつ使うのか。という件です。**

もちろんですが、コンポーネントのメモ化も`useMemo`同様に何らかの条件に基づいて再レンダリングは行われます。その条件というのが、「コンポーネント内の状態に変化があった場合」または「propsに変更があった場合」です。

この「propsに変更があった場合」がポイントです。

Reactでは、以下のように子コンポーネントのprops（`onChange`）として親コンポーネント側で定義された関数（`handleSearch`）を渡すことがあります。

```react
interface ChildProps {
  onChange: (text: string) => void;
}

const Child = React.memo(({ onChange }: ChildProps) => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }
  return <input onChange={(e) => onChange(e.target.value)} />;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");
  const [searchWord, setSearchWord] = useState<string>("");

  const handleSearch = (text: string) => {
    setSearchWord(text);
  };

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <p>Search word: {searchWord}</p>
      <Child onChange={handleSearch} />
    </div>
  );
};
```

`Child`コンポーネントはメモ化されているので、`Parent`が再レンダリングされたとしても`Child`は再レンダリングされないと思いきや、
テキストフォームに入力によってState `name` が更新された際に、レンダリングが重い`Child`も再レンダリングされてしまい、画面がもっさりしてしまいます...

![Screenshot 2024-07-25 at 9.32.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/7a01c546-0d10-3036-2c20-c4b171d784ab.png)


これは、`Parent`が更新された際に`handleSearch`が**再定義**されていることが原因です。

たとえ`handleSearch`の内部処理が全く同じだとしても、React上では再定義が行われた時点で、再定義前の関数とは「異なる」という評価がされます。

これにより`Child`に渡しているprops `onChange` に更新があったと見なされ、`Child`が再レンダリングされてしまうということなのです。

こういった、不要な関数の再定義によるパフォーマンス低下の対策として用意されているものが、`useCallback`なのです！

使い方は簡単です。第一引数に「再定義されてほしくない関数」を指定して、第二引数に「コイツが更新されたら再定義していいよ。なヤツ」を指定します。

大抵の場合、再定義はされてほしくないので第二引数には空配列を指定することがほとんどです。

```react
interface ChildProps {
  onChange: (text: string) => void;
}

const Child = React.memo(({ onChange }: ChildProps) => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }
  return <input onChange={(e) => onChange(e.target.value)} />;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");
  const [searchWord, setSearchWord] = useState<string>("");

  const handleSearch = useCallback((text: string) => {
    setSearchWord(text);
  }, []);

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <p>Search word: {searchWord}</p>
      <Child onChange={handleSearch} />
    </div>
  );
};
```

そうすると、テキストフォームに入力によってState `name` が更新され`Parent`が再レンダリングされたとしても、`Child`は再レンダリングされずに適切にメモ化が維持され、画面のもっさり感も解消されます！👏

![Screenshot 2024-07-25 at 9.33.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/cca1fe6e-8669-e8b0-ecb5-38406ebe3951.png)

このように、`useCallback` は関数の再定義に条件をつけることができます。