React 速習チュートリアル

React useEffect Hook

1. useEffect

useEffect フックを使用すると、コンポーネント内で サイドエフェクト(Side Effects) を実行できるようになります。

サイドエフェクトの具体例としては、データフェッチ(Data Fetching)、DOMの直接更新、タイマーの設定などが挙げられます。

useEffect は2つの引数を受け取ります。第2引数は任意(オプショナル)です。

useEffect(<関数>, <依存配列>)

まずはタイマーを例に考えてみましょう。

1.1 実装例

初期レンダリングの1秒後に setTimeout() を実行してカウントする例です:

import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  });

  return <h1>{count} 回レンダリングされました!</h1>;
}

createRoot(document.getElementById('root')).render(
  <Timer />
);

しかし、ちょっと待ってください!一度だけカウントすればいいはずなのに、カウントが止まりません。

これは、useEffect がレンダリングのたびに実行されるためです。つまり、count が変化するとレンダリングが発生し、それがさらに別のエフェクトをトリガーするというループに陥っています。

これは意図した動作ではありません。サイドエフェクトがいつ実行されるかを制御する方法はいくつかあります。

2. 実行タイミングの制御

useEffect には、第2引数として「配列(Array)」を渡すべきです。この配列の中に依存関係(Dependencies)を含めることで、実行タイミングをコントロールできます。

2.1 依存配列を渡さない場合

useEffect(() => {
  // レンダリングのたびに毎回実行される
});

2.2 空の配列 [] を渡す場合

useEffect(() => {
  // 初回レンダリング時のみ一度だけ実行される
}, []);

2.3 プロップスやステートの値を渡す場合

useEffect(() => {
  // 初回レンダリング時、および指定した依存値が変化した時に実行される
}, [prop, state]);

では、先ほどのタイマーの問題を修正するために、初回レンダリング時のみエフェクトが実行されるようにしてみましょう。

2.4 修正後の実装例

import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []); // <- ここに空の配列を追加します

  return <h1>{count} 回レンダリングされました!</h1>;
}

createRoot(document.getElementById('root')).render(
  <Timer />
);

2.5 変数に依存する実装例

以下は、ある変数に依存する useEffect フックの例です。count 変数が更新されるたびに、エフェクトが再実行されます:

import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

function Counter() {
  const [count, setCount] = useState(0);
  const [calculation, setCalculation] = useState(0);

  useEffect(() => {
    setCalculation(() => count * 2);
  }, [count]); // <- ここに count 変数を追加します

  return (
    <>
      <p>カウント: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <p>計算結果: {calculation}</p>
    </>
  );
}

createRoot(document.getElementById('root')).render(
  <Counter />
);

依存関係が複数ある場合は、それらすべてを useEffect の依存配列に含める必要があります。

3. クリーンアップ処理 (Effect Cleanup)

一部のエフェクトでは、メモリリーク(Memory Leaks)を抑えるために クリーンアップ(Cleanup) が必要です。

タイムアウト(Timeouts)、サブスクリプション(Subscriptions)、イベントリスナーなど、不要になったエフェクトは破棄する必要があります。

これを行うには、useEffect フックの最後に リターン関数(Return Function) を含めます。

3.1 実装例:タイマーのクリーンアップ

import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    let timer = setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);

    // クリーンアップ関数を返す
    return () => clearTimeout(timer)
  }, []);

  return <h1>{count} 回レンダリングされました!</h1>;
}

createRoot(document.getElementById('root')).render(
  <Timer />
);

       注意: タイマーをクリア(解除)するために、timer という名前を変数に割り当てて管理しています。