【React Hooks】ケース毎のuseEffectの挙動

# この記事は?
`useEffect` を使っていて、「この時どんな挙動するんだっけ?」 というのを
思いついたケース分だけ試した時のメモになります。

**各バージョン**

```
    "react": "17.0.2",
    "react-dom": "17.0.2",

    "@types/react": "^17.0.9",
    "@types/react-dom": "^17.0.6",
```

# useEffectの実装
ちなみに `useEffect` の実装はというと [ReactHooks.js](https://github.com/facebook/react/blob/1a3f1afbd3cf815d4e55628cd7d84ef20171bab8/packages/react/src/ReactHooks.js) には↓のように実装されています。

```js
export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}
```

第1引数に処理の実態と、第2引数に影響する配列を指定するお馴染みの形です。

# Case別の挙動
## Case1. 第2引数が空配列

```tsx
import React, { useEffect, useState } from 'react';

const Case1: React.VFC<{ count: number }> = ({ count }) => {
  useEffect(() => {
    console.log('Case1 useEffect.');
    console.log({ count });
  }, []);
  return <>Case1 count: {count}</>;
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  return (
    <div>
      <Case1 count={counter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
    </div>
  );
};

export default Practice;
```

よく書くケースかと思います。
`Case1` を再レンダリングさせる為に、親でカウントアップした値を `Case1` で表示させています。

<img width="300" alt="case1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/f5f2512e-bdc6-c2a1-b989-20645960ede7.gif">


`useEffect` 内が初回のみ実行され、当然カウントの値も初期値が出力されています。

## Case2. 第2引数に指定あり

```tsx
import React, { useEffect, useState } from 'react';

const Case2: React.VFC<{ count: number }> = ({ count }) => {
  useEffect(() => {
    console.log('Case2 useEffect.');
    console.log({ count });
  }, [count]);
  return <>Case2 count: {count}</>;
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  return (
    <div>
      <Case2 count={counter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
    </div>
  );
};

export default Practice;
```

こちらもよくあるケースかと思います。
まずは単純に `useEffect` の第2引数にカウントを指定しています。

<img width="300" alt="case2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/d191b0d2-4992-3ab8-710d-465a56688183.gif">


カウントが更新されたので、`useEffect` 内の処理もそれに合わせて処理が走っています。
次に別の値を`useEffect` 内で参照したらどうなるか試して見たいと思います。

```tsx
import React, { useEffect, useState } from 'react';

const Case2: React.VFC<{ count: number; subCount: number }> = ({
  count,
  subCount,
}) => {
  useEffect(() => {
    console.log('Case2 useEffect.');
    console.log({ count });
    console.log({ subCount });
  }, [count]);
  return (
    <>
      Case2 count: {count} / sub: {subCount}
    </>
  );
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  const [subCounter, setSubCounter] = useState(0);
  return (
    <div>
      <Case2 count={counter} subCount={subCounter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
      <button onClick={() => setSubCounter(subCounter + 1)}>
        サブ カウントアップ
      </button>
    </div>
  );
};

export default Practice;
```
カウンターをもう一つ増やして、異なる値になるようにして試してみます。

<img width="300" alt="case2-2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/1e79cbcd-aa2e-e86c-9b7a-d32cab399333.gif">


最初のカウントが更新されたタイミングのサブカウントの値を参照できています。

## Case3. 第2引数が空だけど、Callbackが呼ばれる

次はちょっと特殊で第2引数を空にして初回しか呼ばれないようにするけど、タイマーを使って
3秒おきにカウンターの値を表示させます。

```tsx
import React, { useEffect, useState } from 'react';

const Case3: React.VFC<{ count: number; subCount: number }> = ({
  count,
  subCount,
}) => {
  useEffect(() => {
    console.log('Case3 useEffect.');
    const timer = setInterval(() => {
      console.log({ count });
      console.log({ subCount });
    }, 3000);

    return () => clearInterval(timer);
  }, []);
  return (
    <>
      Case3 count: {count} / sub: {subCount}
    </>
  );
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  const [subCounter, setSubCounter] = useState(0);
  return (
    <div>
      <Case3 count={counter} subCount={subCounter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
      <button onClick={() => setSubCounter(subCounter + 1)}>
        サブ カウントアップ
      </button>
    </div>
  );
};

export default Practice;
```

<img width="300" alt="case3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/9f1b4d39-b347-f7d1-65e2-1be973282e1f.gif">


カウントを更新しているにも関わらず、タイマーで3秒毎に表示される値は初期値のままになっています。

## Case4. 第2引数に指定ありかつCallbackが呼ばれる

```tsx
import React, { useEffect, useState } from 'react';

const Case4: React.VFC<{ count: number; subCount: number }> = ({
  count,
  subCount,
}) => {
  useEffect(() => {
    console.log('Case4 useEffect.');
    const timer = setInterval(() => {
      console.log({ count });
      console.log({ subCount });
    }, 3000);

    return () => clearInterval(timer);
  }, [count]);
  return (
    <>
      Case4 count: {count} / sub: {subCount}
    </>
  );
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  const [subCounter, setSubCounter] = useState(0);
  return (
    <div>
      <Case4 count={counter} subCount={subCounter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
      <button onClick={() => setSubCounter(subCounter + 1)}>
        サブ カウントアップ
      </button>
    </div>
  );
};

export default Practice;
```

<img width="300" alt="case4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/d2449450-334a-ea04-887e-5da2afd0201d.gif">


`Case3` と `Case4` から第2引数なしの場合は初回の状態を保持、
第2引数ありの場合は第2引数が更新されたタイミングの状態を保持していることが分かりました :eyes:

## Case5. 別のuseEffectから値を更新する

これも特殊で、タイマーでログを出している `useEffect` は第2引数無しで、別の `useEffect` で
カウントが更新されると反応するようにして、内部のカウンターを更新するようにします。

```tsx
import React, { useEffect, useState } from 'react';

const Case5: React.VFC<{ count: number; subCount: number }> = ({
  count,
  subCount,
}) => {
  const [internalCounter, setInternalCounter] = useState(0);
  useEffect(() => {
    console.log('Case5 useEffect.');
    const timer = setInterval(() => {
      console.log({ count });
      console.log({ subCount });
      console.log({ internalCounter });
    }, 3000);

    return () => clearInterval(timer);
  }, []);

  useEffect(() => {
    setInternalCounter(internalCounter + 1);
  }, [count]);
  return (
    <>
      Case5 count: {count} / sub: {subCount} / internal: {internalCounter}
    </>
  );
};

const Practice: React.FC = () => {
  const [counter, setCounter] = useState(0);
  const [subCounter, setSubCounter] = useState(0);
  return (
    <div>
      <Case5 count={counter} subCount={subCounter} />
      <hr />
      <button onClick={() => setCounter(counter + 1)}>カウントアップ</button>
      <button onClick={() => setSubCounter(subCounter + 1)}>
        サブ カウントアップ
      </button>
    </div>
  );
};

export default Practice;
```

<img width="300" alt="case5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/2b5d7d5e-e7c4-a6e5-f36a-acb5eebd33c3.gif">


`Case3`, `Case4` と同じ結果となりました。