ReactのライフサイクルとuseEffectフック

2025-08-08

Reactコンポーネントのライフサイクルと、関数型コンポーネントで副作用を扱うuseEffectフックについて、初心者向けに徹底解説します。コンポーネントの「誕生」から「消滅」までの流れを理解することで、より効果的なReactプログラミングが可能になります。

コンポーネントライフサイクルの基本概念

ライフサイクルとは?

Reactコンポーネントには、マウント(生成)→ 更新 → アンマウント(破棄)という3つの主要な段階があります。各段階で特定のコードを実行できるよう、Reactは「ライフサイクルメソッド」を提供しています。

クラスコンポーネント vs 関数コンポーネント

  • クラスコンポーネントcomponentDidMountcomponentDidUpdatecomponentWillUnmountなどの専用メソッド
  • 関数コンポーネントuseEffectフックで全てのライフサイクル処理をカバー

useEffectの基本構文

最もシンプルなuseEffect

import { useEffect } from 'react';

function ExampleComponent() {
  useEffect(() => {
    // コンポーネントのマウント時と更新時に実行される
    console.log('コンポーネントがレンダリングされました');

    return () => {
      // クリーンアップ関数(後述)
      console.log('コンポーネントがアンマウントされる前や再レンダリング前に実行');
    };
  }); // 依存配列なし(第2引数がない)

  return <div>useEffectの例</div>;
}

useEffectの3つの使用パターン

  1. 依存配列なし:毎回のレンダリング後に実行
  2. 空の依存配列:初回レンダリング後のみ実行
  3. 依存値あり:指定した値が変更された時のみ実行

ライフサイクルシチュエーション別のuseEffect

コンポーネントのマウント時のみ実行(componentDidMount相当)

useEffect(() => {
  console.log('コンポーネントがマウントされました(初回のみ実行)');

  return () => {
    console.log('コンポーネントがアンマウントされます');
  };
}, []); // 空の依存配列

特定のstate/props変更時に実行(componentDidUpdate相当)

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

useEffect(() => {
  console.log(`countが変更されました: ${count}`);
}, [count]); // countが変更された時のみ実行

コンポーネントのアンマウント時処理(componentWillUnmount相当)

useEffect(() => {
  const timer = setInterval(() => {
    console.log('1秒ごとに実行');
  }, 1000);

  return () => {
    clearInterval(timer); // アンマウント時にタイマーをクリア
    console.log('タイマーがクリアされました');
  };
}, []);

useEffectの実践的な使用例

APIデータの取得

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true; // アンマウントフラグ

    const fetchUserData = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();

        if (isMounted) {
          setUser(data);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error('データ取得エラー:', error);
          setLoading(false);
        }
      }
    };

    fetchUserData();

    return () => {
      isMounted = false; // アンマウント時にフラグをfalseに
    };
  }, [userId]); // userIdが変更されたら再取得

  if (loading) return <div>読み込み中...</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

イベントリスナーの登録と解除

function ScrollLogger() {
  useEffect(() => {
    const handleScroll = () => {
      console.log('スクロール位置:', window.scrollY);
    };

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []); // 空の依存配列 = マウント/アンマウント時のみ

  return <div style={{ height: '200vh' }}>スクロールしてコンソールを確認</div>;
}

ドキュメントタイトルの更新

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title;

    return () => {
      document.title = 'デフォルトタイトル'; // オプション: 元に戻す
    };
  }, [title]); // titleが変更されるたびに更新

  return <h1>{title}</h1>;
}

useEffectの依存配列の詳細

依存配列の重要性

依存配列はuseEffectの第2引数で、どの値の変更を監視するかを指定します。

const [count, setCount] = useState(0);
const [text, setText] = useState('');

// パターン1: countが変更された時のみ実行
useEffect(() => { /* ... */ }, [count]);

// パターン2: countまたはtextが変更された時実行
useEffect(() => { /* ... */ }, [count, text]);

// パターン3: 初回のみ実行
useEffect(() => { /* ... */ }, []);

依存配列のよくある間違い

  1. 必要な依存を省略
   const [count, setCount] = useState(0);

   useEffect(() => {
     console.log(`Count: ${count}`);
   }, []); // countを依存配列に入れないと最新値が取得できない
  1. 依存配列を完全に省略
   useEffect(() => {
     console.log('毎回レンダリング後に実行');
   }); // 依存配列なし = 毎回実行 → パフォーマンス問題の原因に
  1. オブジェクト/配列の依存問題
   const user = { id: 1, name: '山田太郎' };

   useEffect(() => {
     console.log(user);
   }, [user]); // 毎回新しいオブジェクトが生成されるため、実質毎回実行

useEffectのクリーンアップ処理

クリーンアップ関数の役割

useEffectのreturnで返す関数は、次のタイミングで実行されます:

  1. コンポーネントがアンマウントされる前
  2. 依存値が変更され、次のuseEffectが実行される前
useEffect(() => {
  // セットアップ処理
  console.log('エフェクトが実行されました');

  return () => {
    // クリーンアップ処理
    console.log('前回のエフェクトがクリーンアップされました');
  };
}, [dependencies]);

クリーンアップが必要なケース

  1. タイマー/インターバルのクリア
   useEffect(() => {
     const timer = setTimeout(() => {
       console.log('タイマー実行');
     }, 1000);

     return () => clearTimeout(timer);
   }, []);
  1. イベントリスナーの解除
   useEffect(() => {
     const handleResize = () => {
       console.log('ウィンドウサイズ:', window.innerWidth);
     };

     window.addEventListener('resize', handleResize);

     return () => {
       window.removeEventListener('resize', handleResize);
     };
   }, []);
  1. APIリクエストのキャンセル
   useEffect(() => {
     const controller = new AbortController();

     const fetchData = async () => {
       try {
         const response = await fetch('/api/data', {
           signal: controller.signal
         });
         // データ処理
       } catch (error) {
         if (error.name !== 'AbortError') {
           console.error('取得エラー:', error);
         }
       }
     };

     fetchData();

     return () => {
       controller.abort(); // リクエストをキャンセル
     };
   }, []);

複数のuseEffectの使用

1つのコンポーネント内で複数のuseEffectを使用することで、関心を分離できます。

function MultiEffectComponent({ userId }) {
  // タイトル更新用
  useEffect(() => {
    document.title = `ユーザー: ${userId}`;
  }, [userId]);

  // データ取得用
  const [user, setUser] = useState(null);
  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      setUser(await response.json());
    };

    fetchUser();
  }, [userId]);

  // ウィンドウイベント用
  useEffect(() => {
    const handleScroll = () => {
      console.log('スクロール中...');
    };

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  // ...
}

よくある間違いとベストプラクティス

無限ループの回避

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

// 無限ループの例(state更新→再レンダリング→useEffect実行→state更新...)
useEffect(() => {
  setCount(count + 1); // 依存配列にcountを含むと無限ループ
}, [count]);

// 解決策1: 依存配列からcountを除外(ただし非推奨)
useEffect(() => {
  setCount(c => c + 1); // 関数型更新を使用
}, []); // 空の依存配列

// 解決策2: 条件付き更新
useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

依存配列の最適化

const [user, setUser] = useState({ id: 1, name: '山田' });

// 非効率的(オブジェクト全体を依存にしている)
useEffect(() => {
  console.log('ユーザー変更');
}, [user]);

// 改善版(必要なプロパティのみを依存に)
useEffect(() => {
  console.log('ユーザーID変更');
}, [user.id]);

カスタムフックでのuseEffect

複雑なロジックはカスタムフックに抽出できます。

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return size;
}

// 使用例
function MyComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      ウィンドウサイズ: {width} x {height}
    </div>
  );
}

useEffectのデバッグ技法

依存関係のデバッグ

useEffect(() => {
  // エフェクトのロジック
}, [dependencies]);

// 依存関係が変更されたか確認
useEffect(() => {
  console.log('依存関係が変更されました:', dependencies);
}, [dependencies]);

実行タイミングの確認

useEffect(() => {
  console.log('エフェクトが実行されました');

  return () => {
    console.log('クリーンアップが実行されました');
  };
}, [dependencies]);

React Developer Toolsの使用

  • コンポーネントの再レンダリングをハイライト表示
  • フックの現在値を確認
  • プロファイラーでパフォーマンスを分析

まとめ:useEffectの重要なポイント

  1. 3つの主要な使用パターン
  • useEffect(fn):毎回のレンダリング後
  • useEffect(fn, []):マウント時のみ
  • useEffect(fn, [dep]):依存値変更時
  1. クリーンアップ関数
  • リソース解放やイベント解除に必須
  • return () => { /* クリーンアップ処理 */ }
  1. 依存配列の適切な管理
  • 必要な依存を過不足なく指定
  • 無限ループに注意
  1. 関心の分離
  • 複数のuseEffectで異なる処理を分離
  1. カスタムフック
  • 複雑なロジックはカスタムフックに抽出

useEffectはReactの副作用管理の要です。適切に使用することで、外部システムとの同期やリソース管理を安全に行えます。初めは難しい概念かもしれませんが、実践を重ねることで自然と理解が深まります。次に学ぶフォーム処理と組み合わせると、さらに実践的なアプリケーション開発が可能になります。