ごまかさずにuseEffectの依存配列を理解する

## はじめに
`useEffect`の依存配列の役割について、しっかりと理解できていますか？

私はなんとなくで実装をしていたため、業務で困った事態に直面することとなりました。

この記事は依存配列によって挙動がどのように変わるかをまとめ、`useEffect`への理解を深めるものです。

### 環境
本記事の実装においては、以下のものを使っています。
| ライブラリ | バージョン | 
|:-|-:|
| React | 18.2.0 |
| TypeScript | 5.2.2 |
| Vite | 5.2.0 |

## useEffectとは

`useEffect`の基本的な作りは
```react
useEffect(実行したい処理, [依存値])
```
となります。
第2引数の依存値の配列は省略可能です。
コンポーネントの読み込み時、および依存値の配列が更新されたときに実行したい処理が実行されます。

`useEffect`の本質的な使い方はここでは割愛します。

依存値の配列（依存配列）の設定方法によって処理の実行タイミングが異なるので、3つの設定パターンを確認します。

### 処理の前提となるコード

3つのコードを用意しました。
冗長になるので、コード部分は折りたたんでいます。

`App.tsx`の`useEffect`の処理をそれぞれのパターンごとに変えて、その挙動の違いを確認します。

<details><summary>前提となるコードを確認する</summary>

```react:App.tsx
import { useEffect, useState } from "react";
import Display from "./Display";
import Register from "./Register";

const App = () => {
  // カウント用
  const [count, setCount] = useState(0);
  const incrementCount = () => setCount(count + 1);
  const decrementCount = () => setCount(count - 1);

  // ラジオボタン用
  const [direction, setDirection] = useState("");

  console.log("component loaded");

  // 処理に合わせて編集
  useEffect();

  return (
    <>
      <Display count={count} direction={direction} />
      <Register
        increment={incrementCount}
        decrement={decrementCount}
        direction={direction}
        setDirection={setDirection}
      />
    </>
  );
};

export default App;
```

```react:Display.tsx
// propsの型
interface DisplayProps {
  count: number;
  direction: string;
}
const Display = ({ count, direction }: DisplayProps) => {
  return (
    <>
      <div>Count:{count}</div>
      <div>Direction:{direction}</div>
    </>
  );
};

export default Display;
```

```react:Register.tsx
import React from "react";
import { ChangeEvent } from "react";

// ラジオボタン定数
const VALUES = ["left", "right"];

// propsの型
interface RegisterProps {
  increment: () => void;
  decrement: () => void;
  direction: string;
  setDirection: (direction: string) => void;
}

const Register = ({
  increment,
  decrement,
  direction,
  setDirection,
}: RegisterProps) => {
  // ラジオボタン変更処理
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setDirection(event.currentTarget.value);
  };

  return (
    <>
      <div>
        <button onClick={increment}>count up</button>
        <button onClick={decrement}>count down</button>
      </div>
      <div>
        {VALUES.map((value) => {
          return (
            <React.Fragment key={value}>
              <label htmlFor={value}>{value}</label>
              <input
                type="radio"
                name="direction"
                id={value}
                value={value}
                checked={value === direction}
                onChange={handleChange}
              />
            </React.Fragment>
          );
        })}
      </div>
    </>
  );
};

export default Register;
```

</details>

上記のコードによって表示される画面は以下のようになります。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/05a55c8c-149c-2584-f127-dfe70cdcc17b.png'>

### 中身のある依存配列を設定しているパターン
```react:Appt.tsxのuseEffectのみ抜粋
  // 依存配列に「count」を設定
  useEffect(() => {
    console.log("execute useEffect");
  }, [count]);
```

依存配列に`count`を設定しています。
よって`useEffect`内に定義した処理は
* 初めの画面読み込み時
* `count`の変更時
に実行されます。

#### 初期読み込み
まず初めの画面読み込み時です。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/f4155163-bfba-27d7-a04c-691a35167653.png'>

「component loaded」はコンポーネントがレンダリングされるたびに出力するように設定しているログです。
比較のために用意しました。

「execute useEffect」は`useEffect`の処理が実行されると出力されるログです。

初めの画面読み込み時なのでこれら2つのログが出力されます。

#### countの変更
「count up」ボタン、または「count down」ボタンを押すことで`count`の値を変更できます。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/63292b49-6b26-32b9-a072-712b4eaaa89a.png'>

まず、「component loaded」が出力されます。
（`count`は`App.tsx`で定義されている`state`で、`state`が更新されるとコンポーネントは再レンダリングされるため。）

そして、依存配列に指定した`count`が変更されたため、`useEffect`の処理も実行され、「execute useEffect」が出力されます。

これは想定通りの挙動です。

#### directionの変更
ラジオボタンの値を変更することで、`direction`を変更できます。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/de643cb6-a26e-a0ec-ed2a-04ad4aaa625f.png'>
`direction`も`state`なのでコンポーネントが再レンダリングされ、「component loaded」が出力されます。

しかし、「execute useEffect」は出力されていません。

コンポーネントが再レンダリングされたとしても、`direction`は依存配列には関係の無い値なので`useEffect`は出力されません。

### 空の依存配列を設定しているパターン
```react:Appt.tsxのuseEffectのみ抜粋
  // 依存配列に空の配列を設定
  useEffect(() => {
    console.log("execute useEffect");
  }, []);
```
中身のない配列を設定した場合はどうなるでしょうか。
依存する値がないため、`useEffect`の実行のためのトリガーは
* 初めの画面読み込み時
のみになります。

#### 初期読み込み
挙動は中身のある依存配列と変わらないので省略します。

#### countの変更
※初めの2行は初期読み込み時の出力です。
`count`の変更によりコンポーネントは再レンダリングされますが、`useEffect`は実行されていません。

<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/4771eed1-4036-c367-b99f-3a4317ad57dc.png'>

#### directionの変更
こちらも`direction`の変更によりコンポーネントが再レンダリングされますが、`useEffect`は実行されません。

<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/1a8d9dc3-014e-a21f-3cb0-36521412fda3.png'>


つまり、依存配列を空にした場合はコンポーネントの初期読み込み時にのみ実行される処理を定義できる、ということになります。

### 依存配列を省略しているパターン
```react:Appt.tsxのuseEffectのみ抜粋
  // 依存配列を省略
  useEffect(() => {
    console.log("execute useEffect");
  });
```

依存配列を省略した場合、空の配列のときと同じで依存する値がないので、初期読み込み時の実行になりそうです。

#### 初期読み込み
挙動は中身のある依存配列と変わらないので省略します。

#### countの変更
※初めの2行は初期読み込み時の出力です。

空の配列のときとは異なり、`useEffect`が実行されていることがわかります。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/c47f2c77-9778-d205-d936-0434f920ad45.png'>

#### directionの変更
こちらも`useEffect`が実行されていることがわかります。
<img width='60%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/610b2c75-84b2-29aa-03f7-6cdee46500bd.png'>

つまり、配列を省略した場合には、コンポーネントが再レンダリングされるたび実行されることになります。

空の配列のときも、配列を省略したときも、どちらも同じような感じがしますがまったく逆の挙動になりました。

## まとめ
依存配列の設定方法について3パターン確認しました。
空の依存配列を設定しているパターンと、依存配列を省略しているパターンを同じと考えていたため、想定外の挙動に悩まされていました。

また、`useEffect`の役割そのものも誤解していたので、ここに関しては別途改めて記事を書いて整理したいと思います。