レッスン一覧に戻る

useEffectの応用

レッスン 3

実際のアプリでよく使うパターン(非同期処理・イベントリスナー・検索など)を通して、 useEffectの応用的な使い方を学びます。

推定学習時間: 30分

🎯 このレッスンで学ぶこと

基礎レッスンで学んだuseEffectの知識を、 実際のアプリケーション開発で使えるように応用していきます。

このレッスンでは、以下の実践的なパターンを学びます:

  • 非同期処理(async/await): API呼び出しなど、時間のかかる処理を正しく扱う方法
  • イベントリスナー: ウィンドウのリサイズやスクロールなどのブラウザイベントを監視する方法
  • デバウンス: 検索フォームなどで、入力が止まってから処理を実行する方法
  • 複数のuseEffect: コードを読みやすく、メンテナンスしやすくする方法
  • よくある失敗パターン: 無限ループなどのバグを避ける方法

💡 このレッスンでは、各パターンを身近な例え話と図解で詳しく説明します。 初心者の方でも、段階的に理解できるように構成されています。

1. 非同期処理(async/await)のパターン

useEffectの中でasync / awaitを使う場合は、中で関数を定義して呼び出す形にするのが定番です。

🤔 なぜ内側で関数を定義するの?

useEffectに直接asyncを付けると、 クリーンアップ関数を返せなくなってしまいます。 内側で関数を定義することで、クリーンアップ関数も使えるようになります。

⚡ async/awaitのパターン比較

間違った書き方

useEffect(async () => { ... }, []);

⚠️ 問題点:

  • useEffectはPromiseを返せない(クリーンアップ関数との相性が悪い)
  • エラーハンドリングが難しい
  • 非推奨な書き方

正しい書き方

useEffect(() => {
  const fetchData = async () => { ... };
  fetchData();
}, []);

✅ メリット:

  • クリーンアップ関数を返せる
  • エラーハンドリングが明確
  • ベストプラクティス

📊 実行フロー

1️⃣

useEffect実行

コンポーネントマウント時に実行

2️⃣

async関数呼び出し

fetchData()を実行

3️⃣

データ取得

APIからデータを取得

4️⃣

状態更新

setUser()で状態を更新

🍕 身近な例えで理解しよう:ピザ注文の例

❌ 間違った書き方: useEffectに直接asyncを付けるのは、「ピザを注文する前に、電話を切ってしまう」ようなものです。 注文が完了する前に処理が終わってしまい、結果を受け取れません。

✅ 正しい書き方: 内側でasync関数を定義するのは、「電話を繋げたまま、ピザを注文し、配達が来るまで待つ」ようなものです。 注文→調理→配達の流れを正しく待つことができます。

📋 流れ: 1. 電話をかける(useEffect実行)→ 2. 注文する(fetchData呼び出し)→ 3. ピザを作る(API呼び出し)→ 4. 配達(状態更新)

📝 正しい書き方(内側にasync関数を定義)

import { useEffect, useState } from "react"; const UserProfile = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchUser = async () => { try { setLoading(true); setError(null); const response = await fetch("/api/user"); if (!response.ok) { throw new Error("レスポンスが正常ではありません"); } const data = await response.json(); setUser(data); } catch (e) { console.error(e); setError("ユーザー情報の取得に失敗しました"); } finally { setLoading(false); } }; fetchUser(); }, []); if (loading) return <p>読み込み中...</p>; if (error) return <p>{error}</p>; if (!user) return <p>ユーザーが見つかりません</p>; return <div>こんにちは、{user.name}さん</div>; };

✅ ポイント1: useEffectに直接asyncを付けないこと(クリーンアップ関数との相性が悪くなるため)

✅ ポイント2: 内側にfetchUser関数を定義して呼び出すのがベストプラクティスです

✅ ポイント3: try-catch-finallyを使って、エラーハンドリングとローディング状態の管理を行います

💡 実践的な使い方

  • ユーザー情報、商品リスト、記事データなどの取得
  • フォーム送信後の処理
  • 定期的なデータ更新(ポーリング)
  • 外部APIとの連携

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

ウィンドウのリサイズやスクロールなど、ブラウザのイベントを監視したいときもuseEffectを使います。

🎯 イベントリスナーとは?

イベントリスナーは、ブラウザで何かが起こった時(ウィンドウがリサイズされた、スクロールした、キーを押したなど)に、 自動的に実行される関数のことです。 例えば、ウィンドウのサイズに応じてレイアウトを変えたい時に使います。

👂 イベントリスナーの登録・解除の流れ

1

コンポーネントマウント

コンポーネントが画面に表示されると、useEffectが実行されます

useEffect(() => { ... }, []);
2

イベントリスナー登録

window.addEventListenerでリサイズイベントを監視します

window.addEventListener("resize", handleResize);
📏

ユーザーがウィンドウをリサイズ

ブラウザウィンドウのサイズが変更されると...

3

ハンドラー実行

リサイズが検知されると、handleResize関数が自動実行されます

setWidth(window.innerWidth);
4

⚠️ クリーンアップ実行

コンポーネントが削除される時、イベントリスナーを解除します

window.removeEventListener("resize", handleResize);

⚠️ 解除しないとメモリリークの原因になります

🎧 身近な例えで理解しよう:ラジオの例

📻 ラジオの例え: イベントリスナーは「ラジオのチャンネルを合わせる」ようなものです。

  • 登録: ラジオのチャンネルを合わせる(addEventListener)→ 音楽が流れ始める
  • イベント発生: ラジオ局から新しい情報が放送される(ウィンドウがリサイズされる)
  • 処理実行: ラジオが情報を受け取って、音量を調整する(handleResizeが実行される)
  • 解除: ラジオを消す前にチャンネルを切る(removeEventListener)→ 電気代の節約

⚠️ 重要なポイント: ラジオを使い終わったら必ず切るのと同じで、イベントリスナーも必ず解除する必要があります。 解除しないと、コンポーネントが削除されてもイベントを監視し続けてしまい、メモリリークが発生します。

📝 画面幅を監視してレイアウトを変える例

import { useEffect, useState } from "react"; const WindowSizeWatcher = () => { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => { setWidth(window.innerWidth); }; window.addEventListener("resize", handleResize); // 初回も一度実行しておく handleResize(); // クリーンアップ関数でリスナーを解除 return () => { window.removeEventListener("resize", handleResize); }; }, []); const isMobile = width < 768; return ( <div> <p>現在の幅: {width}px</p> <p>レイアウト: {isMobile ? "モバイル" : "デスクトップ"}</p> </div> ); };

✅ ポイント1: イベントリスナーは必ずクリーンアップで解除する(メモリリーク防止)

✅ ポイント2: 依存配列を[]にすると、マウント時の1回だけ登録されます

✅ ポイント3: 初回実行もしておくと、初期値が正しく設定されます

⚠️ メモリリークとは?

イベントリスナーを解除しないと、コンポーネントが削除されてもイベントを監視し続けてしまいます。 これがメモリリークです。たくさんのコンポーネントでメモリリークが発生すると、 ブラウザが重くなったり、クラッシュしたりする可能性があります。

3. 入力の監視とデバウンス(検索フォーム)

ユーザーが文字を入力するたびにfetchをすると、リクエストが多くなりすぎます。デバウンス(入力が止まってから少し待って実行)することで改善できます。

🎯 デバウンスとは?

デバウンスは、「入力が止まってから一定時間待ってから処理を実行する」という技術です。 例えば、「React」と入力する場合、R→Re→Rea→Reac→React と5回のAPI呼び出しが発生しますが、 デバウンスを使えば、入力が止まってから1回だけAPIを呼び出せます。

⏱️ デバウンスの仕組み図解

デバウンスなしの場合

ユーザーが1文字入力するたびに、APIを呼び出してしまいます

入力:
R
e
a
c
t
API呼び出し:

⚠️ 5文字入力で5回のAPI呼び出し → サーバーに負荷がかかる

デバウンスありの場合

入力が止まってから500ms待ってから、APIを呼び出します

入力:
R
e
a
c
t
(入力停止)
タイマー:
×
×
×
×
500ms待機
API呼び出し:

✅ 5文字入力で1回のAPI呼び出し → サーバー負荷を大幅に削減

📊 デバウンスの実行フロー

⌨️

1. 文字入力

ユーザーがキーを押す

⏱️

2. タイマー開始

500msのタイマーをセット

🔍

3. 検索実行

入力が止まったら実行

🍜 身近な例えで理解しよう:ラーメン屋の注文の例

❌ デバウンスなし: 「とんこつ」「とんこつしょうゆ」「とんこつしょうゆえび」と、1文字入力するたびに注文を出してしまう。 店員さんが混乱して、大量の注文が発生してしまいます。

✅ デバウンスあり: 「とんこつしょうゆえび」と全部入力して、5秒間何も入力しなかったら注文を確定する。 店員さんは1回だけ注文を受けられて、効率的です。

⏱️ タイマーの役割: 新しい文字が入力されると、前のタイマーをキャンセルして新しいタイマーを開始します。 これは「注文を変更したら、前の注文を取り消して、新しい注文を待つ」ようなものです。

💡 メリット: ユーザーは快適に検索でき、サーバーへの負荷も減らせます。 まさに「Win-Win」の関係です!

📝 500msデバウンス付き検索フォーム

import { useEffect, useState } from "react"; const SearchBox = () => { const [keyword, setKeyword] = useState(""); const [result, setResult] = useState(null); useEffect(() => { if (!keyword) { setResult(null); return; } const timer = setTimeout(() => { // 実際にはここでAPIを呼び出す console.log("検索実行:", keyword); setResult({ message: "検索結果のサンプル" }); }, 500); // 500ms入力が止まったら実行 return () => { clearTimeout(timer); // 前回のタイマーをキャンセル }; }, [keyword]); return ( <div> <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="キーワードを入力" /> {result && <p>{result.message}</p>} </div> ); };

✅ ポイント1: setTimeoutで一定時間待ってから処理を実行します

✅ ポイント2: 新しい入力があると、クリーンアップ関数で前のタイマーをキャンセルします

✅ ポイント3: 空文字の場合は早期リターンして、不要なAPI呼び出しを避けます

💡 ユーザー体験の向上

デバウンスを使うことで、ユーザーは快適に検索でき、サーバーへの負荷も減らせます。 実務で非常によく使われるパターンです。

4. 複数のuseEffectを使い分ける

1つのuseEffectにすべての処理を書くと読みにくくなるので、目的ごとにuseEffectを分けるのがコツです。

🎯 なぜ分けるの?

コードが読みやすくなり、バグの原因を特定しやすくなります。 また、それぞれのuseEffectを個別にテストできたり、 必要に応じて無効化しやすくなったりします。

📚 複数のuseEffectを使い分ける理由

悪い例:1つのuseEffectに全部書く

useEffect(() => {
  // タイトル更新
  document.title = "カウント: " + count;

  // ローカルストレージ保存
  localStorage.setItem("count", String(count));

  // ログ送信
  sendLog(count);
}, [count]);

⚠️ 問題点:

  • 何をしているuseEffectなのか一目で分からない
  • テストが難しい(1つずつテストできない)
  • バグの原因を特定しにくい
  • 再利用が難しい

良い例:役割ごとに分割

📝
タイトル更新用
useEffect(() => {
  document.title = "カウント: " + count;
}, [count]);
💾
ローカルストレージ保存用
useEffect(() => {
  localStorage.setItem("count", String(count));
}, [count]);
📊
ログ送信用
useEffect(() => {
  sendLog(count);
}, [count]);

✅ メリット:

  • 一つ一つのuseEffectが「何のための処理か」明確
  • メンテナンス性が向上
  • 個別にテストできる
  • 必要に応じて無効化しやすい

❌ 悪い例

📦

全部詰め込んだ大きな箱
(何が入っているか分からない)

✅ 良い例

📝
💾
📊

役割ごとに整理された箱
(何が入っているか一目瞭然)

📚 身近な例えで理解しよう:引き出しの整理の例

❌ 悪い例: 全部を1つの引き出しに詰め込むと、何がどこにあるか分からなくなります。 「鉛筆を取り出したい」だけなのに、引き出し全体を開けて探さなければなりません。

✅ 良い例: 文房具用、書類用、工具用と引き出しを分けると、必要なものをすぐに見つけられます。 それぞれの引き出しに「何が入っているか」が明確で、整理整頓されています。

💡 useEffectも同じ: 1つのuseEffectに全部書くのではなく、役割ごとに分けることで、 コードが読みやすくなり、バグの原因も特定しやすくなります。 まるで「整理された引き出し」のような、美しいコードになります!

✅ 良い例(役割ごとに分割)

// タイトル更新用 useEffect(() => { document.title = "カウント: " + count; }, [count]); // ローカルストレージ保存用 useEffect(() => { localStorage.setItem("count", String(count)); }, [count]); // ログ送信用 useEffect(() => { sendLog(count); }, [count]);

✅ メリット1: 一つ一つのuseEffectが「何のための処理か」明確になります

✅ メリット2: メンテナンス性が向上します(変更したい処理だけを探せます)

✅ メリット3: 個別にテストできるようになります

5. よくある失敗パターン:無限ループ

useEffectの中で状態を更新し、 その状態を依存配列に含めていると、無限ループになることがあります。

⚠️ 無限ループとは?

無限ループは、「状態更新 → 再レンダリング → useEffect再実行 → 状態更新 → ...」 が永遠に続いてしまう状態です。ブラウザがフリーズしてしまうこともあります。

🔄 無限ループの仕組みと解決方法

⚠️ 無限ループが発生する流れ

1️⃣レンダリングcount = 02️⃣useEffect実行setCount(0+1)3️⃣状態更新count = 14️⃣再レンダリングcount変更検知

無限ループになるコード

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

useEffect(() => {
  // ❌ 毎回+1してしまう
  setCount(count + 1);
}, [count]); // ← countを依存配列に入れている

⚠️ 何が起こるか:

  1. count = 0 でレンダリング
  2. useEffectが実行され、setCount(0 + 1) で count = 1
  3. countが変わったので、再レンダリング
  4. useEffectが再実行され、setCount(1 + 1) で count = 2
  5. これが永遠に続く... 🔄

解決方法

方法1: 依存配列から除外(1回だけ実行)
useEffect(() => {
  setCount(count + 1);
}, []); // ← 空の配列(1回だけ実行)
方法2: 条件分岐を追加
useEffect(() => {
  if (count < 10) { // ← 条件を追加
    setCount(count + 1);
  }
}, [count]);
方法3: 関数型更新を使う(推奨)
useEffect(() => {
  setCount(prev => prev + 1); // ← 前の値を使う
}, []); // 依存配列を空にできる

🔄 身近な例えで理解しよう:エレベーターの例

❌ 無限ループ: エレベーターに乗って「次の階に行く」ボタンを押し続けるようなものです。 1階→2階→3階...と永遠に上がり続けて、どこにも止まることができません。

✅ 解決方法1: 「最初の1回だけ押す」ようにする(空の依存配列)。 最初の階だけ移動して、そこで止まります。

✅ 解決方法2: 「10階までしか行かない」という条件を付ける(条件分岐)。 10階に着いたら自動的に止まります。

✅ 解決方法3: 「現在の階数を見て、次の階を計算する」ようにする(関数型更新)。 これは最も安全で、推奨される方法です。

💡 重要なポイント: useEffectの中で状態を更新するときは、 依存配列や条件分岐を慎重に設計しないと、無限ループが発生してしまいます。 ブラウザがフリーズしてしまうこともあるので、注意が必要です!

💡 解決方法のまとめ

  • 方法1: 依存配列を空にする(1回だけ実行したい場合)
  • 方法2: 条件分岐を追加する(特定の条件の時だけ実行したい場合)
  • 方法3: 関数型更新を使う(推奨!前の値を使って更新する方法)

📚 まとめ

🎯 覚えておきたいポイント

  1. async/awaitを使う時は、 内側で関数を定義して呼び出す
  2. イベントリスナーは必ずクリーンアップで解除する(メモリリーク防止)
  3. デバウンスを使うと、ユーザー体験とサーバー負荷の両方を改善できる
  4. 複数のuseEffectを 役割ごとに分けると、コードが読みやすくなる
  5. 無限ループを避けるために、依存配列と条件分岐を慎重に設計する