📝

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

2021/06/14に公開

この記事は?

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 には↓のように実装されています。

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引数が空配列

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 で表示させています。

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

Case2. 第2引数に指定あり

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引数にカウントを指定しています。

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

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;

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

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

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

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

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;

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

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

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;

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

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

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

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;

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

Discussion