🎯 このレッスンで学ぶこと
基礎レッスンで学んだuseEffectの知識を、 実際のアプリケーション開発で使えるように応用していきます。
このレッスンでは、以下の実践的なパターンを学びます:
- 非同期処理(async/await): API呼び出しなど、時間のかかる処理を正しく扱う方法
- イベントリスナー: ウィンドウのリサイズやスクロールなどのブラウザイベントを監視する方法
- デバウンス: 検索フォームなどで、入力が止まってから処理を実行する方法
- 複数のuseEffect: コードを読みやすく、メンテナンスしやすくする方法
- よくある失敗パターン: 無限ループなどのバグを避ける方法
💡 このレッスンでは、各パターンを身近な例え話と図解で詳しく説明します。 初心者の方でも、段階的に理解できるように構成されています。
1. 非同期処理(async/await)のパターン
useEffectの中でasync / awaitを使う場合は、中で関数を定義して呼び出す形にするのが定番です。
🤔 なぜ内側で関数を定義するの?
useEffectに直接asyncを付けると、 クリーンアップ関数を返せなくなってしまいます。 内側で関数を定義することで、クリーンアップ関数も使えるようになります。
⚡ async/awaitのパターン比較
間違った書き方
useEffect(async () => { ... }, []);⚠️ 問題点:
- useEffectはPromiseを返せない(クリーンアップ関数との相性が悪い)
- エラーハンドリングが難しい
- 非推奨な書き方
正しい書き方
useEffect(() => {
const fetchData = async () => { ... };
fetchData();
}, []);✅ メリット:
- クリーンアップ関数を返せる
- エラーハンドリングが明確
- ベストプラクティス
📊 実行フロー
useEffect実行
コンポーネントマウント時に実行
async関数呼び出し
fetchData()を実行
データ取得
APIからデータを取得
状態更新
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を使います。
🎯 イベントリスナーとは?
イベントリスナーは、ブラウザで何かが起こった時(ウィンドウがリサイズされた、スクロールした、キーを押したなど)に、 自動的に実行される関数のことです。 例えば、ウィンドウのサイズに応じてレイアウトを変えたい時に使います。
👂 イベントリスナーの登録・解除の流れ
コンポーネントマウント
コンポーネントが画面に表示されると、useEffectが実行されます
useEffect(() => { ... }, []);イベントリスナー登録
window.addEventListenerでリサイズイベントを監視します
window.addEventListener("resize", handleResize);ユーザーがウィンドウをリサイズ
ブラウザウィンドウのサイズが変更されると...
ハンドラー実行
リサイズが検知されると、handleResize関数が自動実行されます
setWidth(window.innerWidth);⚠️ クリーンアップ実行
コンポーネントが削除される時、イベントリスナーを解除します
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を呼び出してしまいます
⚠️ 5文字入力で5回のAPI呼び出し → サーバーに負荷がかかる
デバウンスありの場合
入力が止まってから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再実行 → 状態更新 → ...」 が永遠に続いてしまう状態です。ブラウザがフリーズしてしまうこともあります。
🔄 無限ループの仕組みと解決方法
⚠️ 無限ループが発生する流れ
無限ループになるコード
const [count, setCount] = useState(0);
useEffect(() => {
// ❌ 毎回+1してしまう
setCount(count + 1);
}, [count]); // ← countを依存配列に入れている⚠️ 何が起こるか:
- count = 0 でレンダリング
- useEffectが実行され、setCount(0 + 1) で count = 1
- countが変わったので、再レンダリング
- useEffectが再実行され、setCount(1 + 1) で count = 2
- これが永遠に続く... 🔄
解決方法
方法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: 関数型更新を使う(推奨!前の値を使って更新する方法)
📚 まとめ
🎯 覚えておきたいポイント
async/awaitを使う時は、 内側で関数を定義して呼び出す- イベントリスナーは必ずクリーンアップで解除する(メモリリーク防止)
- デバウンスを使うと、ユーザー体験とサーバー負荷の両方を改善できる
- 複数の
useEffectを 役割ごとに分けると、コードが読みやすくなる - 無限ループを避けるために、依存配列と条件分岐を慎重に設計する