C
React/Hooks/Lesson 09

useEffect

35分·theory
このチャプター
1/5
JavaScript

useEffect

💡 なぜ学ぶ必要があるのか?

🎯 サーバーからデータを取得する最も基本的な方法です。
💼 ページ読み込み時に一度だけ実行したり、特定の値が変わったときだけ実行したりする制御が可能です。
Reactデベロッパーであれば毎日使う必須機能です。
🏢 실무에서는
実務では、コンポーネントが画面に表示されると `useEffect` でAPIを呼び出し、商品一覧やユーザー情報などを取得します。ほぼすべてのウェブアプリがこのパターンで構築されています。

useEffect — コンポーネントのライフサイクル、APIコール/タイマーに使用

日常の比喩: 入退場アテンダント

コンポーネントが画面に表示されたとき(マウント)と消えたとき(アンマウント)に
自動的に実行されるコードを登録するHook。

なぜ学ぶのか?

  • データ読み込み(APIコール)
  • タイマーの設定
  • イベントリスナーの登録/解除

基本構造

js
useEffect(() => {
  // 実行するコード

  return () => {
    // クリーンアップコード — アンマウント時に実行
  };
}, [依存性配列]);

依存配列のパターン

配列実行タイミング
なし毎レンダー後
[]マウント時に一度だけ
[id]id が変わるたびに
💻 マウント時のデータfetch + アンマウント時のクリーンアップ
import { useState, useEffect } from 'react';

// ===== APIデータfetch例 =====
function UserList() {
  // 入力: なし (マウント時に自動実行)
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 処理: マウント時に一度だけ実行 ([]依存性)
    async function fetchUsers() {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await response.json();
      setUsers(data);
      setLoading(false);
    }
    fetchUsers();
  }, []);  // ← 空配列: マウント時に一度だけ

  // 出力: ローディング中 or ユーザーリスト
  if (loading) return <p>読み込み中...</p>;
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// ===== タイマー + クリーンアップ例 =====
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // 処理: 1秒ごとにカウント増加
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // クリーンアップ: コンポーネントアンマウント時にタイマーを削除 (メモリリーク防止)
    return () => clearInterval(timer);
  }, []);  // マウント時に一度だけ設定

  return <p>経過時間: {seconds}秒</p>;
}
💻 良い例 — 2025年推奨パターン(AbortController + 正確な依存配列)
// ✅ ベストプラクティス: React 19 + 2025パターン
const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const abortController = new AbortController();
    
    const loadUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // AbortControllerで競合状態を解決
        const userData = await fetchUser(userId, {
          signal: abortController.signal
        });
        
        // コンポーネントがアンマウントされたか、別のリクエストが開始された場合は中断
        if (!abortController.signal.aborted) {
          setUser(userData);
        }
      } catch (err) {
        if (!abortController.signal.aborted) {
          setError('ユーザー情報の読み込みに失敗しました。');
        }
      } finally {
        if (!abortController.signal.aborted) {
          setLoading(false);
        }
      }
    };

    loadUser();

    // クリーンアップ関数: コンポーネントアンマウントまたはuserId変更時に実行
    return () => {
      abortController.abort();
    };
  }, [userId]); // userId依存性を明示

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
};
💻 応用例 — カスタムHookによる再利用性の向上
// ✅ 2025 高度なパターン: カスタムHook + Suspense互換
function useAsyncData<T>(fetchFn: () => Promise<T>, deps: any[]) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    
    const loadData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const result = await fetchFn();
        
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
          setLoading(false);
        }
      }
    };

    loadData();

    return () => {
      cancelled = true;
    };
  }, deps);

  return { data, loading, error };
}

// 使用例
const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user, loading, error } = useAsyncData(
    () => fetchUser(userId),
    [userId]
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
};

💡 ⚠️ よくある間違い

  • 依存配列に必要な値を省略してESLintの警告を無視する(exhaustive-depsルール違反)
  • クリーンアップ関数なしでsetIntervaladdEventListenerなどを使用してメモリリークが発生する
  • async/awaitをuseEffectのコールバックに直接使用する(useEffectはクリーンアップ関数のみを返す必要がある)
  • オブジェクトや配列を依存関係として使用する際の参照等価性の問題で無限レンダリングが発生する

💡 🎯 面接対策

Q: useEffectの依存配列が空の配列[]の場合と、ない場合の違いを説明してください。
Q: useEffectでAPIコールを行う際、競合状態(Race Condition)をどのように防止できますか?
Q: useEffectのクリーンアップ関数はいつ実行され、なぜ必要なのですか?

ヒント: 依存配列の3つのパターン(なし/空配列/値あり)を明確に区別し、AbortControllerやcleanupフラグを使った競合状態の解決方法をコードとともに説明。メモリリーク防止のためのクリーンアップの重要性を実務事例を交えて回答する。

⚛️ Reactパターン — useEffect

useEffectをReactでどのように使うか、コードとともにステップごとに学んでいきましょう。
1 🎯 1. Effectの定義
レンダー後に実行する副作用
useEffect(() => {
  console.log('マウントまたはcount変更');
}, [count]);
2 📋 2. 依存配列
[] = 一度だけ、[x] = xが変わったとき、なし = 毎回
3 🧹 3. クリーンアップ関数
returnでクリーンアップ関数を返す — アンマウント時に実行
useEffect(() => {
  const t = setInterval(...);
  return () => clearInterval(t);  // cleanup
}, []);
4 ⚠️ 4. よくある間違い
依存関係の漏れ → stale closure / 無限ループ

⏰ useEffect — マウント + 依存関係 + クリーンアップ

コードを直接編集すると0.7秒後に自動的に反映されます。React 18 + Babelでブラウザ上で即時実行。
🖥️ 実行結果 — レンダリングされたReactコンポーネント
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — まず結果を確認し、エディタで自由に編集できます。

確認クイズ

useEffectの依存配列が`[]`(空の配列)のとき、実行タイミングはいつですか?
💡 空の配列 `[]` は「依存する値がない」という意味です。そのため、最初にマウントされたときにちょうど一度だけ実行されます。APIの初期データ読み込みに最もよく使われるパターンです!
先に読むとよい概念: フォーム処理
次のおすすめ: useReducer — Redux の基盤
useEffect - React