🙋‍♂️

useEffectの使用法と考察

に公開

※依存配列の理解について、認識が誤っていたため、8/10に更新しています

ReactのuseEffectについて、学習の記録とを兼ねてまとめてみます。
私なりの理解をなるべく言語化して、考察してみているので、参考にしてもらえたら嬉しいです。

筆者は2025年5月以降、Reactを本格的に学び始めたReact初心者です。
誤った情報を含む可能性もありますので、見つけた際は教えていただけると幸いです。

検証用プロジェクト

私が検証用で作成したプロジェクトです。
よろしければご利用ください。

https://github.com/kaze-wind-dev/practice-react-hook

1. useEffectの基本構造と用途

構文

useEffect(() => {
  // 副作用処理
  return () => {
    // クリーンアップ処理(必要であれば)
  };
}, [依存配列]);

用途

  • DOM操作(例:スクロールイベントの追加)
  • API通信(fetch)
  • タイマー(setInterval)
  • 外部ライブラリの初期化
  • クリーンアップ(removeEventListener、clearInterval など)

Reactでは純粋関数以外の処理は副作用(SideEffects)とされています。
DOM操作やAPIの使用はすべてこの副作用となります。


実行のタイミング

  • マウント時: コールバック関数が実行される
  • 依存配列の値が変更時: 再実行される
  • 再実行前: クリーンアップ関数が実行される(前回の副作用をクリア)
  • アンマウント時: クリーンアップ関数が実行される

依存配列に空の配列を渡すことは、1度限り実行することになるので、再実行を必要としない場合には[]で渡すのが良いです。
また、以下のようなケースは無限ループになる可能性があるので、[]で良いことになります。

React公式では原則として渡すことが推奨されていますが、状態の更新があった際そのため、絶対に更新しないものであれば渡してしまうのが安心。

// 無限ループになってしまうケース
function Counter() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  useEffect(() => {
    setDoubleCount(count * 2); // countを更新
  }, [count, doubleCount]); // doubleCountも依存配列に含めている

  return <div>{doubleCount}</div>;
}
// 修正版
function Counter() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  useEffect(() => {
    setDoubleCount(count * 2);
  }, [count]); // doubleCountは依存配列から除外
}

複数の状態を管理して、再レンダリング後に実行しなければならない場合に、対象となる値を渡すことで、意図した動作になります。

// 依存配列が[]で良いケース
function DataFetcher() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      const result = await api.getData();
      setData(result);
      setLoading(false);
    };
    
    fetchData();
  }, []); // 初回のみ実行
}
// 依存配列に値を渡す必要があるケース
function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const searchProducts = async () => {
      if (!searchTerm.trim()) {
        setResults([]);
        return;
      }
      
      const data = await api.searchProducts({
        term: searchTerm,
        category: category !== 'all' ? category : undefined
      });
      setResults(data);
    };

    searchProducts();
  }, [searchTerm, category]); // 検索条件が変わるたびにAPIを叩いてデータを再取得する必要がある

  return (
    <div>
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)} 
      />
      <select value={category} onChange={(e) => setCategory(e.target.value)}>
        <option value="all">すべて</option>
        <option value="electronics">家電</option>
        <option value="books"></option>
      </select>
      {/* results表示 */}
    </div>
  );
}

2. タイマー制御の例

useEffect(() => {
  if (!timerRunning) return;

  const interval = setInterval(() => {
    setTimer((prev) => prev + 1);
  }, 1000);

  return () => clearInterval(interval);
}, [timerRunning]);

▶ 学んだポイント

  • setInterval のような「継続的副作用」には クリーンアップ関数の記述が必須
  • useEffect の依存配列に状態(例:timerRunning)を含めることで ON/OFF制御が可能
  • React開発環境では StrictModeの影響でuseEffectが2回実行されることがあるため、クリーンアップの重要性が高まる⇒2回実行することで不具合の発生を早めに検知できる

3. 非同期処理(fetch)の扱い方

基本形

useEffect(() => {
  const fetchData = async () => {
    const res = await fetch("https://example.com");
    const json = await res.json();
    setData(json);
  };

  fetchData();
}, []);

▶ 考察・理解

  • ReactはuseEffectの戻り値にPromiseを期待していない⇒リファレンスではundefindが戻り値になっているためPromiseが戻り値になるとエラーになる。
    • useEffect(async () => { ... }) は NG(戻り値がPromiseになり、Reactが誤動作・警告)
    • useEffect(() => { fetchData(); }) という形で、内部でasync関数を定義して実行する
  • 戻り値としてundefinedを維持することがReact的には正しい設計

4. async関数とawaitの関係

▶ 気づきと確認

  • async を付けた関数は、中に await がなくても Promise を返す
  • await を付ける理由は:
    • 関数の中で await を使いたいから(構文的要請)
    • 処理の順番を明示的に制御したいから
  • fetchData() のように async関数を呼んでも、その戻り値(Promise)を useEffectawait する必要はないということ

5. Reactにおけるトップレベルawaitの扱い

▶ 理解

  • 通常のJavaScript(ESMモジュール)では await をトップレベルで使える(Top-Level Await)
  • しかし、Reactのコンポーネント関数では使えない
  • JSXを返すべき関数が Promise を返すようになるため
  • Reactは 純粋関数的に描画関数を扱いたい
  • → 描画は同期的に、非同期は副作用の中で(useEffect)という構造が望ましい

6. 純粋関数との関係(自分の考察)

  • Reactはコンポーネントを「純粋関数(同じ入力→同じ出力)」として扱う前提で設計されている
  • async function Component() にして await fetch() する構造は、副作用と描画を混在させるためReactの哲学に反する
  • 副作用の発生タイミングと描画タイミングを明確に分ける必要がある
  • useEffect はこの「副作用のスコープ化」を行う手段

7. まとめ

  • useEffectは副作用を登録する場所であって、直接非同期関数を渡す場所ではない
  • async関数の実行結果(Promise)はuseEffectの外に返さなければ安全
  • クリーンアップ関数はイベントやリソースの開放に必須

useEffectや使い方について、ほとんどわかっていませんでしたが、完全ではなくとも結構理解することができたと思います。
自分用の記録ではありますが、誰かの参考になれば幸いです。


Discussion