サイトアイコン 【TechGrowUp】

Reactエフェクトのライフサイクル完全ガイド──useEffectがいつ動く?依存関係・クリーンアップ・タイミングを徹底解説

はじめに

Reactコンポーネントで副作用(データ取得・購読・手動DOM操作等)を扱う際の要がuseEffectフックです。しかし「いつ呼ばれるのか」「どこでクリーンアップすべきか」「依存配列の指定をどう書くべきか」を誤解しやすく、バグや無限ループ、不要な再実行に悩む開発者は少なくありません。本稿ではuseEffectuseLayoutEffectのライフサイクルを丁寧に紐解き、実践的なコード例とともに解説します。

Reactエフェクトの基本概念

エフェクト(副作用)とは、ReactのUI更新の外側で起きる処理全般を指します。具体例としては:

useEffectフックは、レンダー後に“副作用”を実行し、必要ならクリーンアップ(解除)も指定できる仕組みです。

import { useEffect } from 'react';

function Example({ url }) {
  useEffect(() => {
    fetch(url).then(r => r.json()).then(data => {
      console.log(data);
    });
  }, [url]);
  return <div>データを取得中…</div>;
}

ライフサイクルフロー全体像

  1. マウント後:最初のレンダーが完了した直後に実行
  2. 更新後:依存配列の値が前回と異なるとレンダー後に再実行
  3. アンマウント前:前回のエフェクトで返したクリーンアップ関数を実行
  4. 依存配列なし:マウント後のみ実行、更新時には再実行されない
  5. 依存配列を省略:レンダーごとに実行し、旧エフェクトは都度クリーンアップ

以下、各ステップを深掘りします。

マウント後の初回実行

useEffect(fn, deps)マウント直後 にまず実行されます。DOMが構築された後なので、document.getElementById など直接DOM操作しても正しく参照できるタイミングです。

function InitLogger() {
  useEffect(() => {
    console.log('コンポーネントが画面に追加されました');
  }, []); // 依存配列空でマウント後のみ
  return <div>初期化ログを出力</div>;
}

依存配列による再実行制御

依存配列 ([a, b, …]) に指定した値が前回と異なる場合、更新後に再実行されます。

function Fetcher({ userId }) {
  const [profile, setProfile] = useState(null);
  useEffect(() => {
    let active = true;
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => {
        if (active) setProfile(data);
      });
    return () => { active = false }; // クリーンアップ
  }, [userId]); 
  // userIdが変わるたびに実行/前回のフェッチはキャンセル
  return profile ? <UserCard data={profile} /> : <p>読み込み中…</p>;
}

クリーンアップ関数の重要性

エフェクト内で何らかの購読(Subscription)やタイマーを登録した場合、コンポーネントの破棄前に必ず解除する必要があります。useEffectコールバックが返す関数がクリーンアップに該当します。

function Clock() {
  const [time, setTime] = useState(Date.now());
  useEffect(() => {
    const id = setInterval(() => setTime(Date.now()), 1000);
    return () => clearInterval(id); // アンマウント時に解除
  }, []);
  return <div>現在時刻:{new Date(time).toLocaleTimeString()}</div>;
}

useLayoutEffectとuseEffectの違い

レイアウトやサイズ計算をDOMに対して行う場合は、レイアウトズレを防ぐために useLayoutEffect を使います。それ以外は通常の useEffect を優先しましょう。

import { useLayoutEffect, useRef } from 'react';

function MeasureBox() {
  const ref = useRef();
  useLayoutEffect(() => {
    console.log(ref.current.getBoundingClientRect());
  }, []);
  return <div ref={ref}>サイズを測定</div>;
}

副次的レンダーとスケジューリング

React 18では厳格モード(Strict Mode)使用時に、開発中の副作用が2回実行される仕様があります。これは初回マウント→アンマウント→再マウントの動きを通して、クリーンアップ漏れを早期に検出するためです。本番ビルドでは1回だけ実行されますが、開発体験向上のため注意して実装しましょう。

パフォーマンス最適化パターン

  1. 依存配列の最小化
    不要な変数を入れると再実行が多発。逆に入れ忘れると stale data の原因。
  2. useCallback / useMemo
    エフェクト中で関数や計算結果を依存させる場合はメモ化して参照の安定化を図る。
  3. サブスクリプション分離
    複数の副作用を一つの useEffect にまとめず、責務ごとに分けると、不要な再実行を抑えやすい。
useEffect(() => { /* データ取得 */ }, [id]);
useEffect(() => { /* 購読登録 */ }, [socket]);

よくある落とし穴と対策

症状原因対策
無限ループ依存配列に頻繁に変わる値が含まれる必要最小限の依存に絞る
クリーンアップ漏れreturn を忘れた購読/タイマーは必ず return で解除
stale closure(古い状態を参照)クロージャ内で古い変数を直接参照依存配列に必要な値をすべて含める、またはフラグ制御
レンダリング前の計測ずれuseEffectでDOM計測useLayoutEffectに移行

実践例:データフェッチ×キャンセルパターン

import { useState, useEffect } from 'react';

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });
    return () => controller.abort(); // クエリ変更時・アンマウント時に中断
  }, [query]);
  return (
    <ul>
      {results.map(r => <li key={r.id}>{r.title}</li>)}
    </ul>
  );
}

まとめ

Reactの副作用ライフサイクルを正しく理解すると、UI更新と非同期処理、リソース管理がスムーズになります。useEffectuseLayoutEffectの使い分け、依存配列の設計、クリーンアップの徹底、パフォーマンス最適化パターンをマスターし、副作用バグから解放された堅牢なコンポーネントを開発しましょう。

モバイルバージョンを終了