更新: 2026年03月 · 10 分で読める · 5,005 文字
React useEffectで副作用を制御する:実装パターンと落とし穴対策
React の useEffect フックは、コンポーネントの副作用を管理するための必須ツールです。この記事では、依存配列の正しい使い方、クリーンアップ関数の実装、無限ループの防止といった実践的なパターンを解説し、すぐにプロダクションコードで応用できる知識を提供します。
useEffect の基本構造を理解する
useEffect は、レンダリング後に副作用を実行するための React フックです。副作用とは、API 呼び出し、DOM 操作、タイマー設定など、コンポーネントの外部に影響を与える処理を指します。
基本的な構文は以下の通りです:
useEffect(() => {
// ここに副作用の処理を記述
console.log('エフェクト実行');
return () => {
// クリーンアップ関数(オプション)
console.log('クリーンアップ実行');
};
}, [依存配列]);
重要なポイントは、useEffect の第二引数である「依存配列」です。この配列の内容によって、エフェクトがいつ実行されるかが決まります。
依存配列を使い分ける 3 つのパターン
パターン 1:マウント時のみ実行(初期化処理)
依存配列を空配列 [] にすると、コンポーネントがマウント時に一度だけ実行されます。API 呼び出しや初期データ取得に最適です:
import React, { useEffect, useState } from 'react';
export function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// マウント時に一度だけ実行
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => console.error(err));
}, []); // 空の依存配列
if (loading) return 読み込み中...
;
return ユーザー名: {user.name};
}
パターン 2:特定の値が変わった時に実行
依存配列に値を指定すると、その値が変わるたびにエフェクトが実行されます。ユーザー ID が変わったときにデータを再取得するケースで有効です:
export function UserDetails({ userId }) {
const [details, setDetails] = useState(null);
useEffect(() => {
// userId が変わるたびに実行
console.log(`ユーザーID ${userId} のデータを取得中...`);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setDetails(data));
}, [userId]); // userId を依存配列に指定
return {details && {details.email}
};
}
パターン 3:毎回実行(依存配列なし)
依存配列を指定しないと、レンダリングのたびにエフェクトが実行されます。通常は避けるべきですが、画面更新のたびに値を同期したい場合に使用します:
useEffect(() => {
// 毎回実行される
console.log('このコンポーネントがレンダリングされました');
}); // 依存配列なし
// ⚠️ これは無限ループの原因になりやすいので注意!
クリーンアップ関数でリソースリークを防ぐ
タイマーやリスナーなど、後処理が必要な場合はクリーンアップ関数を使用します。クリーンアップ関数は、エフェクトが再実行される前、またはコンポーネントがアンマウントされる時に実行されます:
export function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// タイマーを設定
const intervalId = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// クリーンアップ関数
return () => {
clearInterval(intervalId); // タイマーをクリア
console.log('タイマーをクリアしました');
};
}, []);
return 経過秒数: {count}
;
}
クリーンアップ関数を忘れると、タイマーが複数起動したり、イベントリスナーが重複登録されたりするため注意が必要です。
よくあるハマりポイントと対策
無限ループの原因:依存配列にオブジェクトや関数を指定する
JavaScript では、オブジェクトや関数は参照が異なると異なる値として扱われます。そのため、毎回新しいオブジェクトを依存配列に入れると、無限ループが発生します:
// ❌ 悪い例:無限ループが発生
export function BadExample() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, [{ key: 'value' }]); // 毎回新しいオブジェクトが作成される
return {data};
}
// ✅ 良い例:依存配列を修正
export function GoodExample() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, []); // 空の配列で初期化時のみ実行
return {data};
}
stale closure(古いクロージャ)の問題
依存配列が不完全な場合、古い値に基づいて処理が続行される場合があります:
// ❌ 問題のあるコード
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 常に初期値の 0 を出力
}, 1000);
return () => clearInterval(timer);
}, []); // count を依存配列に含めていない
return ;
}
// ✅ 修正版
export function CounterFixed() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 最新の count 値を出力
}, 1000);
return () => clearInterval(timer);
}, [count]); // count を依存配列に含める
return ;
}
useEffect を使うべき場面と使うべきでない場面
使うべき場面
- API からデータを取得する
- DOM に直接アクセスして操作する
- イベントリスナーやタイマーを登録する
- 外部ライブラリと連携する
使うべきでない場面
- イベントハンドラ内の処理(onClick など)→ 直接ハンドラ関数に書く
- 単純な値の変換・計算 →
useMemoを使う - 複数の関連した状態を管理 →
useReducerを使う
useEffect は副作用専用です。単なる計算処理は useMemo、複雑な状態管理は useContext や useReducer を検討してください。
実践例:データ取得 + エラーハンドリング
実際のプロダクション環境で必要なエラーハンドリングとローディング状態を含めたコード例です:
export function DataFetcher({ id }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// abortController で不要なリクエストをキャンセル
const abortController = new AbortController();
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/items/${id}`, {
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// クリーンアップ:コンポーネント アンマウント時またはid変更時にリクエストをキャンセル
return () => abortController.abort();
}, [id]);
if (loading) return 読み込み中...
;
if (error) return エラー: {error}
;
return data ? {JSON.stringify(data)} : データなし
;
}
このコードは以下の重要なパターンを含んでいます:
AbortControllerを使用して、前のリクエストがペンディング中に新しいリクエストが送信される場合にキャンセル- try-catch-finally で適切なエラーハンドリング
- ローディング状態とエラー状態を分離管理
よくある質問
A: 技術的に制限はありませんが、同じロジックは一つの useEffect にまとめるのが実装上は綺麗です。ただし異なる関心事(データ取得と分析トラッキングなど)は、理解しやすさのために別々にしても構いません。
A: 依存配列を正しく指定していれば問題ありません。例えば useState の setter 関数は安定した参照なので依存配列に入れる必要がありません。無限ループは、依存配列が不完全な場合や依存配列を指定しない場合に起こります。
まとめ
useEffectの第二引数(依存配列)で実行タイミングを制御する:空配列でマウント時のみ、値を指定で変更時に実行- クリーンアップ関数を忘れずに実装し、タイマーやリスナーは必ずクリアする
- 無限ループは依存配列の不適切な指定が原因:オブジェクトや関数の参照に注意
- API 呼び出しには
AbortControllerを使用して前のリクエストをキャンセル useEffectは副作用専用:単なる計算はuseMemo、複雑な状態はuseReducerを検討
詳細は React 公式ドキュメント useEffect をご参照ください。
動作環境: macOS 14 / Node.js 20.11 / React 18.2 で動作確認
おすすめフロントエンドリソース
- MDN Web Docs The most trusted reference for HTML, CSS, and JavaScript.
- React Documentation Official React tutorials and API reference.
- Can I Use Essential tool for checking browser compatibility.