useEffect は、関数コンポーネントで副作用を実行できるReactフックであり、クラスコンポーネントのライフサイクルメソッド(componentDidMount、componentDidUpdate、componentWillUnmount)を置き換えます。React Documentation (2025) によると、useEffectはReactがDOMへの変更をコミットした後に実行され、実際のDOMツリーへのアクセスを保証します。このフックはエフェクト関数と、実行頻度を制御するオプションの依存関係配列を受け取ります。
重要なポイント
useEffect はReact 16.8で追加された、関数コンポーネントで副作用を実行するためのフックです。副作用とは、UIレンダリングに直接関係しない操作のことです:APIへのHTTPリクエスト、イベント購読、タイマー処理、DOM操作、ロギング、サードパーティライブラリとの統合などです。
フック以前は、これらの操作はすべてクラスコンポーネントのライフサイクルメソッドに配置する必要がありました:初期化にはcomponentDidMount、プロップの変更に反応するにはcomponentDidUpdate、クリーンアップにはcomponentWillUnmountです。useEffectはこれら3つのシナリオを単一のAPIに統合し、依存関係配列によってエフェクトを実行するタイミングを決定します。これによりロジックが簡素化され、特に購読シナリオでのコード重複が削減されました。
React DevTools Usage Survey (2024) によると、useEffectはuseStateに次いで2番目に人気のあるフックであり、Reactアプリケーションの89%で使用されています。ほとんどの開発者は、データ取得、外部システムとの同期、DOMイベント購読の管理にこれを使用しています。
import { useEffect } from 'react';
function UserProfile({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
}
useEffect は、Reactがレンダリングを完了してDOMを更新した後、渡されたエフェクト関数を実行します。これはレンダリング時の計算との重要な違いです:エフェクトはレンダリングをブロックせず、これは UXパフォーマンス にとって重要です。エフェクトが同期的に実行される場合、ユーザーはデータ読み込み中にフリーズしたインターフェースを見ることになります。
典型的なエフェクトのライフサイクルは3つのフェーズで構成されます。コンポーネントのマウント時に、Reactはエフェクトを実行します。更新のたびに、配列内の少なくとも1つの依存関係が変更された場合、Reactはまず以前のエフェクトのクリーンアップ関数を実行し、次に新しいエフェクトを実行します。コンポーネントのアンマウント時には、クリーンアップ関数のみが実行されます。
React Team — useEffect RFC (2024) によると、useEffectの内部実装はファイバーツリー内の副作用キューを使用しています。変更をコミットした後(コミットフェーズ)、Reactはこのキューを走査し、コンポーネントで宣言された順序でエフェクト関数を呼び出します。各ファイバーノードは、正しいクリーンアップと再実行のために、以前のエフェクトへの参照を保存します。
| 段階 | Reactのアクション | 実行タイミング |
|---|---|---|
| マウント | エフェクト関数を呼び出す | 最初のレンダリング後 |
| 更新 | クリーンアップ → エフェクト | 依存関係が変更されたとき |
| アンマウント | クリーンアップのみ | コンポーネントが削除されたとき |
依存関係配列 — useEffect の第2引数 — は、エフェクトがいつ再実行されるべきかを決定します。ReactはObject.isを使用して、配列内の各値を以前のレンダリングと比較します。少なくとも1つの値が変更された場合、エフェクトが再度実行されます。配列が空([])の場合、エフェクトはマウント後に1回だけ実行されます。
正しい依存関係を選択することは、useEffectを使用する上で最も難しい部分です。配列には、エフェクト内で使用され、レンダリング間で変化する可能性のあるすべての変数と関数を含める必要があります。依存関係を省略すると、stale closure(古いクロージャ)が発生します — エフェクトは以前のレンダリングから古い値を参照します。不要な依存関係を含めると、過剰な再実行 と潜在的なバグが発生します。
// 依存関係がエフェクトの再実行を制御する
useEffect(() => {
document.title = `User: ${user.name}`;
}, [user.name]); // user.nameが変わったときだけ再実行
// eslint-disable-next-line react-hooks/exhaustive-deps
// 依存関係を省略すると古いデータを得る
Reactは eslint-plugin-react-hooks をexhaustive-depsルールとともに提供しており、依存関係配列の完全性を自動的にチェックします。Meta Engineering Blog(2024)によると、このプラグインを有効にすることで、フック関連のバグが72%削減されます。カスタムロジックのある稀なケースを除き、コメントで抑制するのではなく、すべてのexhaustive-deps警告を修正することを推奨します。
依存関係配列をまったく渡さない場合、useEffect はすべてのレンダリング後に実行されます。これはDOM同期やロギングに役立つ場合がありますが、ほとんどの場合はエラーです:エフェクトが頻繁に実行されすぎて、パフォーマンスの低下につながります。ほとんどの場合、空の配列(マウント時に1回)または特定のprops/stateを含む配列を渡す必要があります。
空の配列([])は、エフェクトがどの値にも依存せず、厳密に1回だけ実行されることを意味します。これはクラスコンポーネントのcomponentDidMountに相当します。ただし、注意点:エフェクトが依存関係配列にリストされていないpropsやstateを使用する場合、エフェクトはそれらの初期値を使用し、更新を認識しません。これは stale capture(古い値の取り込み) と呼ばれ、しばしば発見が難しいバグの原因となります。
| 依存関係配列 | 動作 | クラス相当 |
|---|---|---|
| 引数なし | すべてのレンダリング後 | componentDidUpdate |
| [] | マウント時に1回 | componentDidMount |
| [a, b] | aまたはbが変更されたとき | ComponentWillReceiveProps類似 |
| return cleanup | アンマウントを管理 | componentWillUnmount |
クリーンアップ関数は、useEffect がコールバックから返すことができる関数です。Reactはコンポーネントのアンマウント時と、依存関係が変更されたときにエフェクトを再実行する前にこれを呼び出します。クリーンアップは、購読、タイマー、リクエスト、および解放が必要なリソースをキャンセルするために必要です。
典型的な例はWebSocket購読です。マウント時に接続が作成され、依存関係の更新時に再接続され(クリーンアップが古いものを閉じ、エフェクトが新しいものを開く)、アンマウント時に閉じられます。クリーンアップがないと、コンポーネントが再マウントされるたびに新しいWebSocket接続が作成され、メモリリーク と複数の接続が発生します。
useEffect(() => {
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = event => setData(event.data);
// クリーンアップ関数 — アンマウント時および再実行前に実行
return () => {
socket.close();
};
}, []);
React Documentation (2025) によると、AbortControllerはクリーンアップでfetchリクエストをキャンセルするための現代的なアプローチです。エフェクトがHTTPリクエストを行い、コンポーネントが完了前にアンマウントされた場合、リクエストは実行を続け、アンマウント後のsetStateがエラーを引き起こします。エフェクト内でAbortControllerを作成し、クリーンアップでcontroller.abort()を呼び出してリクエストをキャンセルします。
最も一般的な間違いは 依存関係の省略 です。例えば、エフェクトがuserIdプロップを使用しているが、依存関係配列が空の場合です。その結果、エフェクトは初期のuserId値で1回実行され、その変更に反応しません。開発者はコンポーネントが新しいuserIdを受け取っているのを見ますが、データは更新されません。exhaustive-depsルールを持つeslint-plugin-react-hooksはこのようなバグを自動的に検出します。
// ❌ 競合状態 — キャンセルなし
useEffect(() => {
fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);
// ✅ AbortControllerで修正
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => setUser(res));
return () => controller.abort();
}, [userId]);
無限ループの問題を解決するには、以前の状態に基づいて状態を更新するロジックをuseEffectに配置しないでください。setStateの関数形式を使用するか、計算をエフェクトの外に移動します。エフェクトがストレージやブラウザイベントを購読する場合は、リスナーインスタンスが毎回のレンダリングではなく1回だけ作成されるようにします。
よくある質問
直接はできません。useEffect は同期関数またはundefinedの返却を期待するためです。コールバックがasyncとして宣言されると、Promiseが返され、Reactはそれを無視し、クリーンアップメカニズムが機能しなくなります。解決策:エフェクト内で非同期関数を呼び出します:useEffect(() => { async function load() { ... }; load(); }, [])。
制限はありません。Reactは、同じ依存関係配列を持つ場合でも、無関係なロジックを個別の useEffect に分割することを推奨しています。各エフェクトは1つの明確に定義された副作用タスクを担当する必要があります:1つは購読用、もう1つはデータ読み込み用、3つ目はタブタイトルの同期用です。これにより理解とデバッグが容易になります。
React Strict Mode(開発モードのみ)では、すべてのエフェクトがマウント、アンマウント、そして再マウントされます。これは機能であり、バグではありません — Reactがクリーンアップが正しく機能するかどうかをチェックします。アンマウントと再マウント後にエフェクトが正しく動作しない場合(例:重複した購読)、クリーンアップが不完全です。本番環境では、エフェクトは1回だけ実行されます。
AbortController を使用します。エフェクト内でコントローラーを作成し、controller.signalをfetchに渡し、クリーンアップでcontroller.abort()を呼び出します。リクエスト完了前にコンポーネントがアンマウントされた場合、fetchはキャンセルされ、setStateは呼び出されません。これにより競合状態と “Can't perform a React state update on an unmounted component” エラーを防ぎます。
useEffect は例外なくすべてのレンダリング後に実行されます。つまり、エフェクト内のsetStateが新しいレンダリング → 新しいエフェクト → 無限ループを引き起こします。実際には、依存関係配列のないエフェクトはほとんどの場合エラーです。例外は、すべてのレンダリングで同期が必要なロギングや外部システムとの同期です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。