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

2025-08-08

はじめに

この記事では、Reactコンポーネントの ライフサイクル と、関数型コンポーネントで副作用を扱う useEffect フック について、初学者向けにていねいに解説します。

「ライフサイクル」とは、コンポーネントの 誕生(マウント)→ 更新 → 消滅(アンマウント) という一連の流れのことです。たとえば「ページを開いたときにデータを取得したい」「タイマーを設定したがページを離れるときに止めたい」——そういった「コンポーネントの生存期間に合わせた処理」を実装するのが useEffect の役割です。

ライフサイクルとは?3つの段階を理解しよう

Reactコンポーネントには、次の3つの段階があります。

段階意味タイミングの例
マウントコンポーネントが初めて画面に表示されるページを開いたとき・コンポーネントが初めて描画されたとき
更新Props や State が変わって再描画されるボタンをクリックして State が変わったとき
アンマウントコンポーネントが画面から消える別のページに移動したとき・条件分岐で非表示になったとき

関数型コンポーネントでは、これら3つすべてのタイミングを useEffect ひとつで扱えます

useEffectの基本構文

useEffect は「コンポーネントの描画後に何かを実行したいとき」に使います。構文は以下の通りです。

import { useEffect } from 'react';

function ExampleComponent() {
  useEffect(() => {
    // ① ここに「描画後に実行したい処理」を書く
    console.log('コンポーネントが描画されました');

    return () => {
      // ② return する関数は「クリーンアップ関数」(後ほど詳しく説明)
      console.log('次の描画前 or アンマウント前に実行される');
    };
  }); // ③ 第2引数(依存配列)の有無によって実行タイミングが変わる

  return <div>例</div>;
}

構造を3点に分けると理解しやすいです。

  • ①:実行したい処理(データ取得・タイマー設定など)
  • ②:クリーンアップ関数(後片付け。タイマーを止める・イベントを解除するなど)
  • ③:第2引数(依存配列)(「いつ実行するか」を制御する)

useEffectの3つの実行パターン

第2引数の書き方によって、実行タイミングが3通りに変わります。これが useEffect の最重要ポイントです。

パターン1:毎回のレンダリング後に実行(第2引数なし)

useEffect(() => {
  console.log('毎回のレンダリング後に実行される');
}); // 第2引数を書かない

State や Props が変わるたびに毎回実行されます。ほとんどのケースで使うことはなく、パフォーマンスの問題になることもあるため、通常は後述の「空の依存配列」か「値指定」を使います。

パターン2:マウント時のみ実行(空の依存配列)

useEffect(() => {
  console.log('マウント時(初回)のみ実行される');

  return () => {
    console.log('アンマウント時に実行される');
  };
}, []); // 空の配列 = 「変化を監視しない」 = 初回だけ

最もよく使うパターンです。「ページを開いたときにデータを取得する」「タイマーを設定する」などに使います。

パターン3:特定の値が変わったときに実行(依存配列に値を指定)

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

useEffect(() => {
  console.log(`count が変わった: ${count}`);
}, [count]); // count が変わったときだけ実行

配列内に指定した値が変わったときだけ実行されます。「選択中のユーザーIDが変わったらデータを再取得する」などのケースで使います。

クリーンアップ関数とは?

クリーンアップ関数とは、useEffect の中で return する関数のことです。主に「起動したものを後片付けする」ために使います。

実行されるタイミングは2つあります。

  • コンポーネントがアンマウントされる直前
  • 依存値が変わって次の useEffect が実行される直前
useEffect(() => {
  console.log('① エフェクト開始');

  return () => {
    console.log('② クリーンアップ(次のエフェクト前 or アンマウント前)');
  };
}, [依存値]);

クリーンアップが必要な代表的なケースを3つ挙げます。

ケース1:タイマーのクリア

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

  // アンマウント時にタイマーを止めないと、
  // コンポーネントが消えた後でもタイマーが動き続けてしまう
  return () => clearTimeout(timer);
}, []);

ケース2:イベントリスナーの解除

useEffect(() => {
  const handleResize = () => {
    console.log('ウィンドウサイズ:', window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  // 解除しないとメモリリークの原因になる
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

ケース3:APIリクエストのキャンセル

useEffect(() => {
  // AbortController でリクエストをキャンセルできる
  const controller = new AbortController();

  const fetchData = async () => {
    try {
      const response = await fetch('/api/data', {
        signal: controller.signal // キャンセル信号を渡す
      });
      const data = await response.json();
      console.log(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('取得エラー:', error);
      }
    }
  };

  fetchData();

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

実践例1:APIデータの取得

最も頻繁に使う useEffect の用途が「APIデータの取得」です。以下は userId が変わるたびにユーザー情報を取得する例です。

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let isMounted = true; // このフラグでアンマウント後の setState を防ぐ

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

        // アンマウントされた後に setState しようとするとエラーになるため
        // isMounted が true のときだけ更新する
        if (isMounted) {
          setUser(data);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error('データ取得エラー:', error);
          setLoading(false);
        }
      }
    };

    fetchUserData();

    return () => {
      isMounted = 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>
  );
}

このコードで押さえるべきポイントは3つです。

  • userId を依存配列に入れることで、ID が変わるたびに自動的に再取得する
  • isMounted フラグでアンマウント後の State 更新を防ぐ(これを忘れると「メモリリーク」の警告が出ることがある)
  • loading State で読み込み中の表示を切り替える

実践例2:スクロール位置の監視

グローバルなイベントリスナーを設定する場合の典型的なパターンです。

import { useState, useEffect } from 'react';

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollY(window.scrollY);
    };

    // マウント時にリスナーを登録
    window.addEventListener('scroll', handleScroll);

    // アンマウント時に必ず解除する
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []); // 空配列 = マウント/アンマウントのみ

  return (
    <div style={{ position: 'fixed', top: 10, right: 10 }}>
      スクロール: {scrollY}px
    </div>
  );
}

実践例3:ページタイトルの動的更新

シンプルで覚えやすい例です。Props で受け取った title が変わるたびにブラウザのタイトルを更新します。

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title; // ブラウザのタブのタイトルを変更

    return () => {
      document.title = 'Reactアプリ'; // アンマウント時に元に戻す
    };
  }, [title]); // title が変わるたびに実行

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

複数のuseEffectで関心を分離する

1つのコンポーネントに複数の useEffect を書くことができます。「別の目的の処理は別の useEffect に書く」ことで、コードが整理されて読みやすくなります。

function MultiEffectComponent({ userId }) {
  const [user, setUser] = useState(null);

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

  // ② データ取得専用
  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);
  }, []);

  return <div>{user?.name}</div>;
}

1つの useEffect に全部まとめると依存配列の管理が複雑になります。目的ごとに分けるのがベストプラクティスです。

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

依存配列は useEffect で最も間違いが起きやすい部分です。代表的なパターンを確認しておきましょう。

間違い1:依存値を入れ忘れる

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

// ❌ count を使っているのに依存配列に入れていない
useEffect(() => {
  console.log(`Count: ${count}`); // count は常に初期値 0 のまま
}, []); // 空配列だと count の最新値を参照できない

// ✅ 正しい
useEffect(() => {
  console.log(`Count: ${count}`);
}, [count]);

間違い2:依存配列を省略してしまう(毎回実行)

// ❌ 第2引数なし = 毎回レンダリング後に実行される(意図しない多発)
useEffect(() => {
  fetch('/api/data'); // レンダリングのたびにAPIを叩いてしまう
});

// ✅ 意図した実行タイミングを第2引数で明示する
useEffect(() => {
  fetch('/api/data');
}, []); // マウント時のみ

間違い3:無限ループになってしまう

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

// ❌ count を依存配列に入れ、かつ count を更新している → 無限ループ
useEffect(() => {
  setCount(count + 1); // count が変わる → useEffect が実行 → count が変わる → ...
}, [count]);

// ✅ 解決策:条件を付けてループを止める
useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

カスタムフックでuseEffectを再利用する

同じ useEffect のロジックを複数のコンポーネントで使い回したいときは、カスタムフックとして切り出すことができます。カスタムフックは「use で始まる関数」として定義します。

// カスタムフックとして定義(use から始める命名ルールがある)
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;
}

// 使用側:useEffect の詳細を知らなくても使える
function MyComponent() {
  const { width, height } = useWindowSize();

  return <p>ウィンドウサイズ: {width} × {height}</p>;
}

カスタムフックにすることで、複雑な useEffect のロジックを隠ぺいし、使う側のコードがすっきりします。

まとめ:useEffectの使い分け一覧

やりたいこと書き方
マウント時のみ実行(データ取得・タイマー設定など)useEffect(fn, [])
特定の値が変わったときに実行useEffect(fn, [値])
アンマウント時に後片付けをするuseEffect(() => { return () => { /* 後片付け */ }; }, [])
毎回のレンダリング後に実行(使う機会は少ない)useEffect(fn)(第2引数なし)

次の「Reactのフォーム処理:Controlled Components」では、useEffect と State を組み合わせた、より実践的なフォームの実装方法を学びます。


演習問題

この記事で学んだ「ライフサイクルと useEffect」を定着させるための練習問題です。初級・中級・上級の3段階、全12問を収録しています。

出題形式について:まず全12問を読んで自力でコードを書いてみてください。解答例と解説は、この後の「📘 解説(全12問)」セクションにまとめてあります。


🟢 初級問題(問題1〜3)

useEffect の基本的な3パターンと、クリーンアップ関数の書き方を練習します。

問題1:マウント時にコンソールログを出そう

【問題】
以下の条件をすべて満たす MountLogger コンポーネントを作成してください。

  • useEffect を使い、コンポーネントがマウントされたとき(初回のみ)に console.log("コンポーネントがマウントされました") を実行する
  • 同じ useEffect のクリーンアップ関数で console.log("コンポーネントがアンマウントされます") を実行する
  • 画面には <p>ライフサイクルを確認してください</p> を表示する
  • ブラウザの開発者コンソールで正しいタイミングにログが出ることを確認すること

【期待するコンソール出力(マウント時)】

コンポーネントがマウントされました

問題2:countが変わるたびにタイトルを更新しよう

【問題】
以下の条件をすべて満たす TitleCounter コンポーネントを作成してください。

  • State として count(数値、初期値: 0)を管理する
  • useEffect を使い、count が変わるたびに document.title"カウント: 〇〇"(〇〇は count の値)に更新する
  • ボタン「+1」をクリックすると count が増える
  • useEffect の依存配列には count のみを指定すること

【期待する動作】:ボタンを押すたびにブラウザのタブのタイトルが「カウント: 1」「カウント: 2」と更新される。


問題3:1秒ごとにカウントアップするタイマーを作ろう

【問題】
以下の条件をすべて満たす AutoTimer コンポーネントを作成してください。

  • State として seconds(数値、初期値: 0)を管理する
  • useEffect を使い、マウント時に setInterval で1秒ごとに seconds を1増やす
  • クリーンアップ関数で clearInterval を呼び、アンマウント時にタイマーを止める
  • 画面に "経過時間: 〇〇秒" と表示する
  • setInterval のコールバックでは State 更新の関数形式(prev => prev + 1)を使うこと

【期待する表示(3秒後)】

経過時間: 3秒

🟡 中級問題(問題4〜9)

依存配列の正しい使い方・スクロール・ウィンドウイベントへの対応・フェッチとローディング表示など、実践的なパターンに取り組みます。

問題4:依存配列の3パターンを書き比べよう

【問題】
以下の条件をすべて満たす EffectPatterns コンポーネントを作成してください。

  • State として count(数値、初期値: 0)と name(文字列、初期値: "React")を管理する
  • 以下の3つの useEffect をそれぞれ独立して実装する
useEffect依存配列ログ内容
Aなし(第2引数なし)"[A] 毎回実行: count=〇〇, name=〇〇"
B[](空)"[B] マウント時のみ実行"
C[count]"[C] countが変わった: 〇〇"
  • 「countを増やす」ボタンと「nameを変える」ボタンを用意し、どの useEffect が実行されるかコンソールで確認する

問題5:スクロール位置をリアルタイムに表示しよう

【問題】
以下の条件をすべて満たす ScrollTracker コンポーネントを作成してください。

  • State として scrollY(数値、初期値: 0)を管理する
  • useEffect でマウント時に windowscroll イベントリスナーを登録し、window.scrollYscrollY に反映する
  • クリーンアップ関数でイベントリスナーを解除する
  • スクロール位置を画面の右上に固定表示する(position: 'fixed' を使うこと)
  • ページを縦に長くするためのダミーコンテンツ(高さ 200vh 程度の div)も入れること

【期待する表示】

(右上に固定)スクロール: 342px

問題6:ウィンドウのリサイズを検知しよう

【問題】
以下の条件をすべて満たす WindowSize コンポーネントを作成してください。

  • State として width(数値)と height(数値)を、window.innerWidthwindow.innerHeight で初期化する
  • useEffect でマウント時に resize イベントリスナーを登録し、リサイズのたびに widthheight を更新する
  • クリーンアップ関数でリスナーを解除する
  • 現在のウィンドウサイズを "幅: 〇〇px / 高さ: 〇〇px" の形式で表示する

【期待する表示】

幅: 1280px / 高さ: 720px

問題7:JSONPlaceholderからデータを取得して表示しよう

【問題】
以下の条件をすべて満たす PostList コンポーネントを作成してください。

  • State として posts(配列、初期値: [])と loading(真偽値、初期値: true)を管理する
  • マウント時に https://jsonplaceholder.typicode.com/posts?_limit=5 から5件の投稿データを取得する
  • 取得完了後、posts を更新し loadingfalse にする
  • ローディング中は "読み込み中..." を表示する
  • 取得後は各投稿の title<li> で表示する
  • useEffect の依存配列は [](マウント時のみ)とすること

【期待する表示(取得後)】

• sunt aut facere repellat provident...
• qui est esse
• ea molestias quasi exercitationem...
• eum et est occaecati
• nesciunt quas odio sit aut non

問題8:選択したユーザーIDに応じてデータを切り替えよう

【問題】
以下の条件をすべて満たす UserSwitcher コンポーネントを作成してください。

  • State として userId(数値、初期値: 1)と user(オブジェクト or null、初期値: null)と loading(真偽値、初期値: true)を管理する
  • useEffect の依存配列に userId を指定し、userId が変わるたびに https://jsonplaceholder.typicode.com/users/${userId} からデータを取得する
  • ボタン「前のユーザー」「次のユーザー」で userId を1〜10の範囲で変更できるようにする(1未満・10超にならないようにすること)
  • 取得した usernameemailcompany.name を表示する

【期待する表示】

ユーザー ID: 3 [← 前] [次 →]
Clementine Bauch
Nathan@yesenia.net
Romaguera-Jacobson

問題9:オンライン/オフライン状態を検知しよう

【問題】
以下の条件をすべて満たす NetworkStatus コンポーネントを作成してください。

  • State として isOnline(真偽値、初期値: navigator.onLine)を管理する
  • useEffect でマウント時に online イベントと offline イベントのリスナーを window に登録する
  • online イベントで isOnlinetrue に、offline イベントで false にする
  • クリーンアップ関数で両方のリスナーを解除する
  • isOnlinetrue なら "🟢 オンライン"false なら "🔴 オフライン" と表示する

【期待する表示(オンライン時)】

ネットワーク状態: 🟢 オンライン

🔴 上級問題(問題10〜12)

カスタムフック・複数の useEffect の組み合わせ・APIの中断処理など、実際の開発に近いシナリオに挑戦します。

問題10:useFetchカスタムフックを作ろう

【問題】
以下の条件をすべて満たすカスタムフック useFetch と、それを使うコンポーネントを作成してください。

  • useFetch(url) は引数として URL を受け取り、{ data, loading, error } を返す
  • 内部で useEffect を使い、url が変わるたびにデータを取得する
  • 取得中は loading: true、成功時は data: レスポンス、失敗時は error: エラーメッセージ をセットする
  • AbortController を使ってアンマウント時にリクエストをキャンセルする
  • 作成した useFetch を使って https://jsonplaceholder.typicode.com/todos/1 のデータを表示するコンポーネントを作る

【期待する表示(取得後)】

タイトル: delectus aut autem
完了状態: 未完了

問題11:複数のuseEffectで関心を分離したダッシュボードを作ろう

【問題】
以下の条件をすべて満たす Dashboard コンポーネントを作成してください。

  • State として userName(文字列、初期値: "田中太郎")・visitCount(数値、初期値: 0)・windowWidth(数値、初期値: window.innerWidth)を管理する
  • 以下の3つの useEffectそれぞれ独立して実装する
useEffect処理内容依存配列
document.title"ダッシュボード - 〇〇さん" に更新する[userName]
マウント時に visitCount を1増やし、コンソールに "訪問回数: 1" と出力する[]
resize イベントで windowWidth を更新する(クリーンアップ必須)[]
  • 画面に userNamevisitCountwindowWidth を表示する
  • ユーザー名を変更する input も追加する

問題12:検索クエリが変わるたびにAPIを叩くサジェスト機能を作ろう

【問題】
以下の条件をすべて満たす SearchSuggest コンポーネントを作成してください。

  • State として query(文字列、初期値: "")・results(配列、初期値: [])・loading(真偽値、初期値: false)を管理する
  • useEffect の依存配列に query を指定し、query が変わるたびに検索を実行する
  • query が空のときは何もしない(results を空にして return する)
  • query が1文字以上のとき、https://jsonplaceholder.typicode.com/users から全ユーザーを取得し、namequery が含まれるもの(大文字小文字を区別しない)だけ results にセットする
  • AbortController でアンマウント or query 変更時にリクエストをキャンセルする
  • 入力欄・ローディング表示・結果リストを表示する

【期待する動作(”e” を入力した場合)】

検索: [e              ]

• Leanne Graham
• Clementine Bauch
• Chelsey Dietrich
• ...

📘 解説(全12問)

各問題の解答例と解説です。自分のコードと見比べて確認してください。


解説1:マウント時にコンソールログを出そう

【解答例】

import { useEffect } from 'react';

function MountLogger() {
  useEffect(() => {
    console.log("コンポーネントがマウントされました");

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

  return <p>ライフサイクルを確認してください</p>;
}

【解説】

  • useEffect(() => {...}, []) の空配列が「マウント時のみ」のキーポイント
  • return () => {...} がクリーンアップ関数。アンマウント時に自動的に呼ばれる
  • React の開発モード(Strict Mode)では、バグを発見するためにエフェクトが2回実行されることがある。本番では1回のみ

解説2:countが変わるたびにタイトルを更新しよう

【解答例】

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `カウント: ${count}`;
  }, [count]); // count が変わるたびに実行

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

【解説】

  • 依存配列に [count] を指定することで、count が変化したときだけ useEffect が再実行される
  • document.title の変更は「Reactの外側への副作用」であり、useEffect の中に書くのが正しい
  • レンダリングのたびに実行したい場合は依存配列を省略するが、今回のケースでは count の変化時のみで十分

解説3:1秒ごとにカウントアップするタイマーを作ろう

【解答例】

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds(prev => prev + 1); // 関数形式で最新値を参照
    }, 1000);

    return () => clearInterval(id); // アンマウント時にタイマーを止める
  }, []); // 空配列 = マウント時に1回だけセットアップ

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

【解説】

  • setInterval の戻り値(タイマーID)を変数に保存し、クリーンアップで clearInterval(id) に渡す
  • setSeconds(prev => prev + 1) の関数形式を使う理由:setInterval のコールバックは閉包で古い seconds を参照してしまうため、常に最新の値で更新するために関数形式が必要
  • 依存配列を [] にすることで、インターバルはマウント時の1回だけセットされる

解説4:依存配列の3パターンを書き比べよう

【解答例】

import { useState, useEffect } from 'react';

function EffectPatterns() {
  const [count, setCount] = useState(0);
  const [name,  setName]  = useState("React");

  // A: 毎回のレンダリング後(第2引数なし)
  useEffect(() => {
    console.log(`[A] 毎回実行: count=${count}, name=${name}`);
  });

  // B: マウント時のみ(空の依存配列)
  useEffect(() => {
    console.log("[B] マウント時のみ実行");
  }, []);

  // C: count が変わったときのみ
  useEffect(() => {
    console.log(`[C] countが変わった: ${count}`);
  }, [count]);

  return (
    <div>
      <p>count: {count} / name: {name}</p>
      <button onClick={() => setCount(count + 1)}>countを増やす</button>
      <button onClick={() => setName(name === "React" ? "Vue" : "React")}>nameを変える</button>
    </div>
  );
}

【解説】

  • 「countを増やす」ボタンを押すと A・C が実行され、「nameを変える」ボタンを押すと A のみが実行される。B はマウント時の1回だけ
  • A のような「依存配列なし」は、不要な処理が毎回走るためパフォーマンスに悪影響を与えやすい。意図的に使う場面は少ない
  • 3パターンの違いを実際に動かして確認することが一番の近道

解説5:スクロール位置をリアルタイムに表示しよう

【解答例】

import { useState, useEffect } from 'react';

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);

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

  return (
    <>
      <div style={{ position: 'fixed', top: 10, right: 10, background: '#fff', padding: '4px 8px', border: '1px solid #ccc' }}>
        スクロール: {scrollY}px
      </div>
      <div style={{ height: '200vh' }}>
        <p>スクロールしてみてください</p>
      </div>
    </>
  );
}

【解説】

  • window.addEventListener はグローバルなイベントなので、コンポーネントが消えてもリスナーが残り続ける。クリーンアップでの解除が必須
  • position: 'fixed' でスクロールに追従しない固定要素を作れる
  • 依存配列が [] なのは、ハンドラーを登録するのはマウント時の1回だけでよいから

解説6:ウィンドウのリサイズを検知しよう

【解答例】

import { useState, useEffect } from 'react';

function WindowSize() {
  const [width,  setWidth]  = useState(window.innerWidth);
  const [height, setHeight] = useState(window.innerHeight);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    };

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

  return <p>幅: {width}px / 高さ: {height}px</p>;
}

【解説】

  • useState(window.innerWidth) のように初期値を動的な値(現在のウィンドウ幅)にできる
  • resize イベントは連続して発火するため、本番では debounce を使って発火頻度を制限することが多い
  • 問題10のカスタムフック化に挑戦する場合の足がかりにもなる

解説7:JSONPlaceholderからデータを取得して表示しよう

【解答例】

import { useState, useEffect } from 'react';

function PostList() {
  const [posts,   setPosts]   = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('取得エラー:', error);
      } finally {
        setLoading(false); // 成功・失敗どちらでも loading を false に
      }
    };

    fetchPosts();
  }, []); // マウント時のみ

  if (loading) return <p>読み込み中...</p>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

【解説】

  • useEffect の中で async を直接使えないため、内部に async 関数を定義して呼び出す
  • finally ブロックを使うと、成功・失敗どちらでも loadingfalse にできる
  • 依存配列が [] なので、ページを開いた瞬間に1回だけ取得される

解説8:選択したユーザーIDに応じてデータを切り替えよう

【解答例】

import { useState, useEffect } from 'react';

function UserSwitcher() {
  const [userId,  setUserId]  = useState(1);
  const [user,    setUser]    = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    const fetchUser = async () => {
      try {
        const res  = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        const data = await res.json();
        setUser(data);
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]); // userId が変わるたびに再取得

  return (
    <div>
      <p>
        ユーザー ID: {userId}
        <button onClick={() => setUserId(id => Math.max(1, id - 1))}>← 前</button>
        <button onClick={() => setUserId(id => Math.min(10, id + 1))}>次 →</button>
      </p>
      {loading ? <p>読み込み中...</p> : user && (
        <div>
          <p>{user.name}</p>
          <p>{user.email}</p>
          <p>{user.company.name}</p>
        </div>
      )}
    </div>
  );
}

【解説】

  • 依存配列に [userId] を入れることで、IDが変わるたびに自動的に再フェッチされる
  • Math.max(1, id - 1)Math.min(10, id + 1) で範囲外に出ないようにする
  • useEffect の最初に setLoading(true) を呼ぶことで、ID切り替えのたびに「読み込み中」表示が出る

解説9:オンライン/オフライン状態を検知しよう

【解答例】

import { useState, useEffect } from 'react';

function NetworkStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const goOnline  = () => setIsOnline(true);
    const goOffline = () => setIsOnline(false);

    window.addEventListener('online',  goOnline);
    window.addEventListener('offline', goOffline);

    return () => {
      window.removeEventListener('online',  goOnline);
      window.removeEventListener('offline', goOffline);
    };
  }, []);

  return (
    <p>ネットワーク状態: {isOnline ? "🟢 オンライン" : "🔴 オフライン"}</p>
  );
}

【解説】

  • navigator.onLine で現在のオンライン状態を取得できる。これを State の初期値にすることで、最初から正しい状態が表示される
  • 2つのリスナーを登録しているため、クリーンアップでも2つ解除する必要がある
  • このパターンは問題10のカスタムフック(useOnlineStatus など)に切り出すと再利用しやすい

解説10:useFetchカスタムフックを作ろう

【解答例】

import { useState, useEffect } from 'react';

// カスタムフック:use から始める命名ルール
function useFetch(url) {
  const [data,    setData]    = useState(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    const fetchData = async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => controller.abort(); // URLが変わったりアンマウントされたらキャンセル
  }, [url]);

  return { data, loading, error };
}

// 使用例
function TodoDetail() {
  const { data: todo, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

  if (loading) return <p>読み込み中...</p>;
  if (error)   return <p>エラー: {error}</p>;

  return (
    <div>
      <p>タイトル: {todo.title}</p>
      <p>完了状態: {todo.completed ? "完了" : "未完了"}</p>
    </div>
  );
}

【解説】

  • カスタムフックは「use で始まる関数」として定義し、内部でフックを使える
  • AbortController を使うと、コンポーネントがアンマウントされたり URL が変わったりしたときに進行中のリクエストをキャンセルできる
  • data, loading, error の3点セットは API フェッチの定番パターン。覚えておこう

解説11:複数のuseEffectで関心を分離したダッシュボードを作ろう

【解答例】

import { useState, useEffect } from 'react';

function Dashboard() {
  const [userName,    setUserName]    = useState("田中太郎");
  const [visitCount,  setVisitCount]  = useState(0);
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  // ① タイトル更新:userName が変わるたびに実行
  useEffect(() => {
    document.title = `ダッシュボード - ${userName}さん`;
  }, [userName]);

  // ② 訪問カウント:マウント時のみ1回
  useEffect(() => {
    setVisitCount(1);
    console.log("訪問回数: 1");
  }, []);

  // ③ リサイズ監視:マウント/アンマウントのみ
  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div>
      <input
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
        placeholder="ユーザー名"
      />
      <p>ユーザー名: {userName}</p>
      <p>訪問回数: {visitCount}回</p>
      <p>ウィンドウ幅: {windowWidth}px</p>
    </div>
  );
}

【解説】

  • 3つの useEffect がそれぞれ独立した責務を持ち、依存配列も異なる。まとめて1つにするよりずっと読みやすい
  • ①は userName の入力に即座に反応し、②・③は互いに干渉しない
  • このパターンをさらに進めると、①②③をそれぞれカスタムフック化できる(useDocumentTitleuseWindowWidth など)

解説12:検索クエリが変わるたびにAPIを叩くサジェスト機能を作ろう

【解答例】

import { useState, useEffect } from 'react';

function SearchSuggest() {
  const [query,   setQuery]   = useState("");
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (query.trim() === "") {
      setResults([]); // 空なら結果をクリア
      return;
    }

    const controller = new AbortController();
    setLoading(true);

    const search = async () => {
      try {
        const res   = await fetch('https://jsonplaceholder.typicode.com/users', {
          signal: controller.signal,
        });
        const users = await res.json();
        const filtered = users.filter((u) =>
          u.name.toLowerCase().includes(query.toLowerCase())
        );
        setResults(filtered);
      } catch (err) {
        if (err.name !== 'AbortError') console.error(err);
      } finally {
        setLoading(false);
      }
    };

    search();

    return () => controller.abort(); // query が変わったら前のリクエストをキャンセル
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="ユーザー名で検索..."
      />
      {loading && <p>検索中...</p>}
      <ul>
        {results.map((u) => <li key={u.id}>{u.name}</li>)}
      </ul>
    </div>
  );
}

【解説】

  • query が空のときに早期 return することで、不要な API 呼び出しを避けられる
  • AbortController をクリーンアップで使うと、素早くタイプしたときに前の検索リクエストをキャンセルできる(競合状態の防止)
  • 本番では、入力のたびに API を叩かないよう debounce(一定時間後に実行)を組み合わせることが多い