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 更新を防ぐ(これを忘れると「メモリリーク」の警告が出ることがある)loadingState で読み込み中の表示を切り替える
実践例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でマウント時にwindowのscrollイベントリスナーを登録し、window.scrollYをscrollYに反映する- クリーンアップ関数でイベントリスナーを解除する
- スクロール位置を画面の右上に固定表示する(
position: 'fixed'を使うこと) - ページを縦に長くするためのダミーコンテンツ(高さ
200vh程度のdiv)も入れること
【期待する表示】
(右上に固定)スクロール: 342px
問題6:ウィンドウのリサイズを検知しよう
【問題】
以下の条件をすべて満たす WindowSize コンポーネントを作成してください。
- State として
width(数値)とheight(数値)を、window.innerWidthとwindow.innerHeightで初期化する useEffectでマウント時にresizeイベントリスナーを登録し、リサイズのたびにwidth・heightを更新する- クリーンアップ関数でリスナーを解除する
- 現在のウィンドウサイズを
"幅: 〇〇px / 高さ: 〇〇px"の形式で表示する
【期待する表示】
幅: 1280px / 高さ: 720px
問題7:JSONPlaceholderからデータを取得して表示しよう
【問題】
以下の条件をすべて満たす PostList コンポーネントを作成してください。
- State として
posts(配列、初期値:[])とloading(真偽値、初期値:true)を管理する - マウント時に
https://jsonplaceholder.typicode.com/posts?_limit=5から5件の投稿データを取得する - 取得完了後、
postsを更新しloadingをfalseにする - ローディング中は
"読み込み中..."を表示する - 取得後は各投稿の
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超にならないようにすること) - 取得した
userのname・email・company.nameを表示する
【期待する表示】
ユーザー ID: 3 [← 前] [次 →]
Clementine Bauch
Nathan@yesenia.net
Romaguera-Jacobson
問題9:オンライン/オフライン状態を検知しよう
【問題】
以下の条件をすべて満たす NetworkStatus コンポーネントを作成してください。
- State として
isOnline(真偽値、初期値:navigator.onLine)を管理する useEffectでマウント時にonlineイベントとofflineイベントのリスナーをwindowに登録するonlineイベントでisOnlineをtrueに、offlineイベントでfalseにする- クリーンアップ関数で両方のリスナーを解除する
isOnlineがtrueなら"🟢 オンライン"、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 を更新する(クリーンアップ必須) | [] |
- 画面に
userName・visitCount・windowWidthを表示する - ユーザー名を変更する
inputも追加する
問題12:検索クエリが変わるたびにAPIを叩くサジェスト機能を作ろう
【問題】
以下の条件をすべて満たす SearchSuggest コンポーネントを作成してください。
- State として
query(文字列、初期値:"")・results(配列、初期値:[])・loading(真偽値、初期値:false)を管理する useEffectの依存配列にqueryを指定し、queryが変わるたびに検索を実行するqueryが空のときは何もしない(resultsを空にして return する)queryが1文字以上のとき、https://jsonplaceholder.typicode.com/usersから全ユーザーを取得し、nameにqueryが含まれるもの(大文字小文字を区別しない)だけ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ブロックを使うと、成功・失敗どちらでもloadingをfalseにできる- 依存配列が
[]なので、ページを開いた瞬間に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の入力に即座に反応し、②・③は互いに干渉しない - このパターンをさらに進めると、①②③をそれぞれカスタムフック化できる(
useDocumentTitle・useWindowWidthなど)
解説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(一定時間後に実行)を組み合わせることが多い