Chapter 07

useEffect

APIからのデータ取得やタイマーなど、コンポーネントの外の世界と連携する「副作用」をuseEffectで管理する方法を学びます。

60 min

このチャプターで学ぶこと

  • 副作用(side effect)とは何かを説明できる
  • useEffectの依存配列の役割と3つのパターンを理解できる
  • クリーンアップ関数を使ってメモリリークを防げる
  • APIからデータを取得してStateに保存できる
  • ローディング状態とエラー状態を適切に管理できる
  • JSONPlaceholderからユーザー一覧を取得して表示できる

useEffect

これまで学んだ State とイベントハンドリングで、かなりインタラクティブなUIが作れるようになりました。でも実際のアプリには欠かせない「APIからのデータ取得」がまだです。このチャプターでは useEffect を使って外の世界と連携する方法を学びます。

副作用(Side Effect)とは

React のコンポーネントは本来、props と state を受け取り、JSX を返すだけの「純粋な関数」であるべきです。

しかし実際には、コンポーネントがこんなことをしたい場合があります:

これらはReactのレンダリングサイクルの「外側」にある処理で、副作用(Side Effect) と呼ばれます。useEffect はこれらの副作用を管理するためのフックです。

📝 Laravelで例えると

Laravelのコントローラーで例えると、レスポンスを返す処理(return view(...))が「純粋な処理」で、ログへの書き込み・キャッシュの更新・メール送信などが「副作用」のイメージに近いです。

副作用はメインの処理の「外側」で起きる、付随的な処理です。

useEffect の基本構文

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // ここに副作用の処理を書く
    console.log('コンポーネントがレンダリングされた');
  });

  return <div>...</div>;
}

useEffect は2つの引数を取ります:

  1. 実行したい関数(副作用の処理)
  2. 依存配列(いつ実行するかを制御する配列)

依存配列の3パターン

依存配列の書き方によって、useEffect がいつ実行されるかが変わります。

パターン1: 空の配列 [] → マウント時に1回だけ

useEffect(() => {
  // コンポーネントが初めて表示されたとき(マウント時)に1回だけ実行
  console.log('マウントされた');
  fetchData(); // APIからのデータ取得など
}, []); // ← 空の配列

使いどき: 初回データ取得、イベントリスナーの登録、タイマーの開始など

パターン2: 値を指定 [dep] → 値が変わるたびに実行

const [userId, setUserId] = useState(1);

useEffect(() => {
  // userId が変わるたびに実行される
  fetchUser(userId);
}, [userId]); // ← userId を監視

使いどき: IDが変わったときにデータを再取得する、検索ワードが変わったら検索し直すなど

パターン3: 依存配列なし → 毎レンダリング後に実行

useEffect(() => {
  // レンダリングのたびに実行(ほとんど使わない)
  document.title = `カウント: ${count}`;
}); // ← 配列なし
⚠️ 依存配列なしは基本的に避ける

依存配列を省略すると、毎回のレンダリング後に実行されます。APIの呼び出しなどを書くと無限ループになる可能性があるので注意が必要です。依存配列は基本的に必ず書きましょう。

3パターンのまとめ

// マウント時に1回だけ
useEffect(() => { ... }, []);

// userId が変わるたびに
useEffect(() => { ... }, [userId]);

// 毎レンダリング後(ほぼ使わない)
useEffect(() => { ... });

クリーンアップ関数

useEffect ではタイマーやイベントリスナーを設定することがあります。コンポーネントが画面から消えるとき(アンマウント時)に、これらを**片付ける(クリーンアップする)**必要があります。

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // タイマーを設定
    const intervalId = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // クリーンアップ関数を返す
    // コンポーネントがアンマウントされるときに実行される
    return () => {
      clearInterval(intervalId); // タイマーを止める
      console.log('タイマーをクリーンアップした');
    };
  }, []); // マウント時に1回だけ設定

  return <p>経過時間: {seconds}</p>;
}

クリーンアップ関数を返すことで、コンポーネントが消えたあともタイマーが動き続けるという問題(メモリリーク)を防げます。

💡 クリーンアップが必要なケース
  • setInterval / setTimeoutclearInterval / clearTimeout
  • addEventListenerremoveEventListener
  • WebSocket 接続 → 切断処理
  • 外部ライブラリの初期化 → 破棄処理

クリーンアップを怠ると、コンポーネントが画面から消えたあとも処理が走り続け、メモリリークやバグの原因になります。

APIからデータを取得する

いよいよ実践的な例です。useEffect を使ってAPIからデータを取得し、State に保存して表示する方法を学びましょう。

テスト用のAPIとして JSONPlaceholderhttps://jsonplaceholder.typicode.com)を使います。無料で使えるフェイクAPIで、ユーザー、投稿、コメントなどのダミーデータを返してくれます。

import { useState, useEffect } from 'react';

function UserList() {
  // データを保存するState
  const [users, setUsers] = useState([]);
  // ローディング状態
  const [isLoading, setIsLoading] = useState(true);
  // エラー状態
  const [error, setError] = useState(null);

  useEffect(() => {
    // 非同期関数を定義してすぐ呼ぶ
    async function fetchUsers() {
      try {
        setIsLoading(true);
        setError(null);

        const response = await fetch('https://jsonplaceholder.typicode.com/users');

        // レスポンスのステータスを確認
        if (!response.ok) {
          throw new Error(`HTTPエラー: ${response.status}`);
        }

        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    }

    fetchUsers();
  }, []); // マウント時に1回だけ取得

  // ローディング中
  if (isLoading) {
    return <p>読み込み中...</p>;
  }

  // エラー発生時
  if (error) {
    return <p>エラーが発生しました: {error}</p>;
  }

  // データ表示
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <strong>{user.name}</strong>
          <br />
          <small>{user.email}</small>
        </li>
      ))}
    </ul>
  );
}
📝 useEffect の中で async/await を使うには

useEffect のコールバック関数を直接 async にすることはできません。

// NG: useEffect のコールバックを直接 async にしない
useEffect(async () => {
  const data = await fetchData(); // 動くけど推奨されない
}, []);

// OK: 内部に async 関数を作ってすぐ呼ぶ
useEffect(() => {
  async function fetchData() {
    const data = await fetch(...);
    // ...
  }
  fetchData(); // すぐ呼ぶ
}, []);

理由は、useEffect のコールバックはクリーンアップ関数(同期関数)か undefined を返す必要があるためです。async 関数は常に Promise を返すので、型が合いません。

ローディングと エラー状態を管理する理由

Laravelでビューを返すときは、コントローラーでデータを用意してからビューをレンダリングするので「データがない状態」はほぼ発生しません。

でもReactではコンポーネントがレンダリングされた後にデータを取得するため:

  1. 最初はデータなし(空の配列や null)で表示される
  2. データ取得中はローディングを表示
  3. 取得成功したらデータを表示
  4. 取得失敗したらエラーを表示

この3〜4つの状態を管理するのが基本パターンです。

より実用的な例:IDを変えて再取得する

ユーザーを選択したら、そのユーザーの詳細を取得する例です。

import { useState, useEffect } from 'react';

function UserDetail() {
  const [selectedUserId, setSelectedUserId] = useState(1);
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  // selectedUserId が変わるたびに実行される
  useEffect(() => {
    async function fetchUser() {
      setIsLoading(true);
      try {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${selectedUserId}`
        );
        const data = await res.json();
        setUser(data);
      } catch (err) {
        console.error('取得失敗:', err);
      } finally {
        setIsLoading(false);
      }
    }

    fetchUser();
  }, [selectedUserId]); // selectedUserId を依存配列に入れる

  return (
    <div>
      {/* ユーザーIDを切り替えるボタン */}
      <div>
        {[1, 2, 3, 4, 5].map((id) => (
          <button
            key={id}
            onClick={() => setSelectedUserId(id)}
            style={{ fontWeight: selectedUserId === id ? 'bold' : 'normal' }}
          >
            ユーザー {id}
          </button>
        ))}
      </div>

      {/* ユーザー詳細 */}
      {isLoading ? (
        <p>読み込み中...</p>
      ) : user ? (
        <div>
          <h2>{user.name}</h2>
          <p>メール: {user.email}</p>
          <p>電話: {user.phone}</p>
          <p>会社: {user.company?.name}</p>
        </div>
      ) : null}
    </div>
  );
}
💡 依存配列に何を入れるか

依存配列には「useEffect の中で使っている、変化しうる値」を入れます。

// selectedUserId を useEffect の中で使っているので依存配列に入れる
useEffect(() => {
  fetchUser(selectedUserId); // selectedUserId を使っている
}, [selectedUserId]); // だから依存配列に入れる

依存配列に入れるのを忘れると、古い値を使い続けてバグになります。ESLintの react-hooks/exhaustive-deps ルールを有効にすると、自動で警告してくれます。

ブラウザのタイトルを更新する例

副作用のシンプルな例として、ページタイトルを変更するものも見ておきましょう。

import { useState, useEffect } from 'react';

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

  // count が変わるたびにタイトルを更新
  useEffect(() => {
    document.title = `カウント: ${count} - My App`;

    // クリーンアップ: コンポーネントが消えたらタイトルをリセット
    return () => {
      document.title = 'My App';
    };
  }, [count]);

  return (
    <div>
      <p>ブラウザのタブタイトルが変わるよ: {count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
    </div>
  );
}
✍ やってみよう

演習: JSONPlaceholderからユーザー一覧を取得しよう

https://jsonplaceholder.typicode.com/users から10人のユーザーを取得して、カードスタイルで表示するコンポーネントを作りましょう。

localhost:5173
JSONPlaceholderから取得したユーザー一覧
APIから取得した10人のユーザーをカード表示

要件:

  • ローディング中は「読み込み中…」を表示する
  • エラー時は「データの取得に失敗しました」と表示する
  • 各ユーザーカードには名前・メールアドレス・電話番号・会社名を表示する
  • ユーザー名の頭文字をアバターとして表示する(例:「田中太郎」→「田」)

ユーザーデータの構造:

// JSONPlaceholder が返すユーザーデータの形
{
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "Sincere@april.biz",
  phone: "1-770-736-8031 x56442",
  company: {
    name: "Romaguera-Crona",
    // ...
  },
  // ...
}

スターターコード:

import { useState, useEffect } from 'react';

function UserCard({ user }) {
  // 名前の最初の1文字をアバターとして使う
  const initial = user.name.charAt(0);

  return (
    <div style={{
      border: '1px solid #e2e8f0',
      borderRadius: '8px',
      padding: '16px',
      marginBottom: '12px',
      display: 'flex',
      alignItems: 'center',
      gap: '16px',
    }}>
      {/* アバター */}
      <div style={{
        width: '48px',
        height: '48px',
        borderRadius: '50%',
        backgroundColor: '#4f46e5',
        color: 'white',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '20px',
        fontWeight: 'bold',
        flexShrink: 0,
      }}>
        {initial}
      </div>

      {/* ユーザー情報 */}
      <div>
        <h3 style={{ margin: 0 }}>{user.name}</h3>
        <p style={{ margin: '4px 0', color: '#6b7280' }}>{user.email}</p>
        <p style={{ margin: '4px 0', color: '#6b7280' }}>{user.phone}</p>
        <p style={{ margin: 0, fontSize: '14px' }}>
          会社: {user.company.name}
        </p>
      </div>
    </div>
  );
}

function UserList() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUsers() {
      // ここに実装しよう
    }

    fetchUsers();
  }, []);

  // ローディング・エラー・データ表示のハンドリングを追加しよう

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h1>ユーザー一覧</h1>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

export default UserList;

チャレンジ:

  • 検索ボックスを追加して、名前でリアルタイムフィルタリングできるようにしよう(useEffect は不要、filter() だけでOK)
  • 「再読み込み」ボタンを追加しよう(ヒント: 再取得をトリガーするためのStateを使う)
  • PokeAPI(https://pokeapi.co/api/v2/pokemon?limit=20)に切り替えて、ポケモン一覧を表示してみよう

useEffect のよくあるミス

ミス1: 依存配列を忘れて無限ループ

// NG: 無限ループになる!
function BadComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(result => {
      setData(result); // State を更新
    });
    // 依存配列がないと毎レンダリング後に実行 → State更新 → レンダリング → ...
  }); // ← 依存配列がない!

  return <div>{data}</div>;
}

// OK: [] を付けてマウント時だけ実行
useEffect(() => {
  fetchData().then(result => {
    setData(result);
  });
}, []); // ← 空の配列

ミス2: クリーンアップを忘れる

// NG: コンポーネントが消えてもタイマーが動き続ける
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // クリーンアップなし!
}, []);

// OK: クリーンアップ関数を返す
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id); // ← 忘れずに
}, []);

まとめ

これで React の基本フック(useState + useEffect)をマスターしました。次は Props と State を組み合わせた実践的なコンポーネント設計を学んでいきましょう!