
React useEffect 完全ガイド - セットアップ・クリーンアップ・依存配列を徹底解説
useEffectとは何か
useEffectは、Reactコンポーネントを外部システムと同期させるためのフックです。
「外部システム」とは、React自身が管理していないものすべてを指します:
- ブラウザAPI(
document.title、localStorage、addEventListenerなど) - サーバーへのAPI呼び出し
- WebSocket接続
- サードパーティライブラリ
- タイマー(
setInterval、setTimeout)
重要: useEffectは「Reactの外」に出るための**脱出ハッチ(Escape Hatch)**です。Reactだけで完結する処理には使うべきではありません。
基本構文
useEffect(() => {
// セットアップ関数(副作用を実行)
return () => {
// クリーンアップ関数(オプション)
};
}, [dependencies]); // 依存配列3つの構成要素
| 要素 | 説明 |
|---|---|
| セットアップ関数 | コンポーネントがDOMに追加された後に実行される |
| クリーンアップ関数 | コンポーネントがDOMから削除される前、または次のセットアップ前に実行される |
| 依存配列 | 再実行のトリガーとなる値のリスト |
依存配列の3つのパターン
1. 依存配列なし:毎回実行
useEffect(() => {
console.log('毎回のレンダリング後に実行');
});使いどころ: ほとんどありません。通常は依存配列を指定すべきです。
2. 空の依存配列:マウント時のみ
useEffect(() => {
console.log('マウント時に1回だけ実行');
return () => {
console.log('アンマウント時に実行');
};
}, []);使いどころ:
- アプリ起動時の初期化処理
- イベントリスナーの登録
- 外部サービスへの接続
3. 依存配列あり:依存値が変わったら実行
useEffect(() => {
console.log(`countが ${count} に変わりました`);
}, [count]);使いどころ:
- propsやstateの変化に応じた処理
- 特定の値に基づくデータフェッチ
- URLパラメータの変化への対応
実践例:チャットルーム接続
最も典型的なuseEffectの使用例を見てみましょう。
import { useState, useEffect } from 'react';
import { createConnection } from './chat';
function ChatRoom({ roomId }: { roomId: string }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
// セットアップ: 接続を確立
const connection = createConnection(serverUrl, roomId);
connection.connect();
// クリーンアップ: 接続を切断
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // serverUrlまたはroomIdが変わったら再実行
return (
<div>
<p>接続先: {serverUrl}</p>
<p>ルーム: {roomId}</p>
</div>
);
}実行フロー
1. コンポーネントがマウント
→ セットアップ実行(接続)
2. roomIdが "general" → "music" に変化
→ クリーンアップ実行(旧接続を切断)
→ セットアップ実行(新接続を確立)
3. コンポーネントがアンマウント
→ クリーンアップ実行(接続を切断)
クリーンアップ関数の重要性
クリーンアップ関数は「後片付け」を行います。これを忘れるとメモリリークや予期しない動作が発生します。
必ずクリーンアップが必要なケース
イベントリスナー
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// 必須: リスナーを解除
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);タイマー
useEffect(() => {
const intervalId = setInterval(() => {
console.log('1秒ごとに実行');
}, 1000);
// 必須: タイマーをクリア
return () => {
clearInterval(intervalId);
};
}, []);API呼び出し(競合状態の防止)
useEffect(() => {
let isCancelled = false;
async function fetchData() {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
// キャンセルされていなければstateを更新
if (!isCancelled) {
setUser(data);
}
}
fetchData();
// クリーンアップ: キャンセルフラグを立てる
return () => {
isCancelled = true;
};
}, [userId]);WebSocket
useEffect(() => {
const ws = new WebSocket('wss://example.com/socket');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// 必須: 接続を閉じる
return () => {
ws.close();
};
}, []);依存配列のルール
ルール1: Effect内で使う全ての値を含める
// Good: 正しい: 全ての依存を含む
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
// Bad: 間違い: roomIdが依存配列にない
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl]); // roomIdが変わっても再接続されない!ルール2: 不要な依存を減らすにはコードを修正
依存配列から値を「削除」するのではなく、Effect自体を修正して依存を減らします。
// Bad: 関数を依存に含めると毎回実行される
function ChatRoom({ roomId }) {
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 毎回新しい関数が作られる
}
// Good: 関数をEffect内に移動
function ChatRoom({ roomId }) {
useEffect(() => {
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomIdだけが依存
}ルール3: オブジェクトと配列は参照が変わる
// Bad: 毎回新しいオブジェクトが作られる
function Component() {
const options = { theme: 'dark' }; // 毎レンダリングで新しい参照
useEffect(() => {
applyTheme(options);
}, [options]); // 毎回実行される!
}
// Good: 解決策1: プリミティブ値を依存にする
function Component() {
const theme = 'dark';
useEffect(() => {
applyTheme({ theme });
}, [theme]); // 文字列は値で比較される
}
// Good: 解決策2: useMemoでオブジェクトをメモ化
function Component() {
const options = useMemo(() => ({ theme: 'dark' }), []);
useEffect(() => {
applyTheme(options);
}, [options]);
}useEffectが不要なケース
公式ドキュメントには「You Might Not Need an Effect」というページがあります。以下のケースではuseEffectを使うべきではありません。
1. 計算値の導出
// Bad: 悪い例: useEffectで計算
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
// Good: 良い例: レンダリング中に計算
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 単純に計算すればよい
const fullName = firstName + ' ' + lastName;
return <p>{fullName}</p>;
}理由: useEffectを使うと「レンダリング → Effect実行 → state更新 → 再レンダリング」と2回レンダリングされます。
2. propsの変化に応じたstateリセット
// Bad: 悪い例: useEffectでリセット
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
// Good: 良い例: keyを使ってコンポーネントをリセット
function App() {
const [items, setItems] = useState([...]);
return <List items={items} key={items.id} />;
}
function List({ items }) {
const [selection, setSelection] = useState(null);
// keyが変わるとコンポーネント全体がリマウントされる
}3. ユーザーイベントの処理
// Bad: 悪い例: useEffectでイベント処理
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
if (product) {
// 購入処理
sendAnalytics('purchase', product.id);
}
}, [product]);
return (
<button onClick={() => setProduct(...)}>
購入
</button>
);
}
// Good: 良い例: イベントハンドラで処理
function ProductPage({ productId }) {
const handleBuy = () => {
// 購入処理
sendAnalytics('purchase', productId);
};
return (
<button onClick={handleBuy}>
購入
</button>
);
}4. アプリ起動時の初期化(1回だけ)
// Bad: 悪い例: useEffectで初期化(Strict Modeで2回実行される)
function App() {
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
}
// Good: 良い例: モジュールスコープで初期化
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
}
// Good: さらに良い例: アプリ起動前に初期化
if (typeof window !== 'undefined') {
loadDataFromLocalStorage();
checkAuthToken();
}
function App() {
// ...
}Strict Modeでの動作
React 18以降のStrict Mode(開発環境)では、useEffectは意図的に2回実行されます。
1回目: マウント → セットアップ実行
2回目: クリーンアップ実行 → 再マウント → セットアップ実行
目的: クリーンアップ関数が正しく実装されているかを検証するためです。
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect(); // これが正しく実装されていないと問題が発覚する
};
}, []);Strict Modeで2回実行されても問題ないEffect = 正しく実装されたEffect
Next.js(App Router)での注意点
Next.jsのApp Routerでは、Server ComponentsとClient Componentsの区別が重要です。
Server Componentsではフックは使えない
// app/page.tsx (Server Component)
// Bad: エラー: Server ComponentではuseEffectは使えない
export default function Page() {
useEffect(() => {
// ...
}, []);
return <div>...</div>;
}Client Componentで使う
// app/page.tsx
import ClientComponent from './ClientComponent';
export default function Page() {
return <ClientComponent />;
}
// app/ClientComponent.tsx
'use client'; // Client Componentを明示
import { useEffect, useState } from 'react';
export default function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// クライアントサイドでのみ実行される
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}SSR時の注意: typeof windowチェック
'use client';
useEffect(() => {
// useEffect内はクライアントサイドでのみ実行されるので
// windowチェックは不要
window.localStorage.setItem('key', 'value');
}, []);
// ただし、useEffect外でwindowを参照する場合はチェックが必要
const isClient = typeof window !== 'undefined';よくあるバグと解決策
1. 無限ループ
// Bad: 無限ループ
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // stateを更新 → 再レンダリング → Effect実行 → ...
}, [count]);
// Good: 解決策: 関数形式でstateを更新
useEffect(() => {
setCount(c => c + 1);
}, []); // 依存配列からcountを除外できる2. 古いstateを参照(stale closure)
// Bad: 古いcountを参照してしまう
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 常に0が表示される
}, 1000);
return () => clearInterval(id);
}, []); // countが依存にない
// Good: 解決策1: 依存配列に含める
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);
// Good: 解決策2: 関数形式でstateにアクセス
useEffect(() => {
const id = setInterval(() => {
setCount(c => {
console.log(c); // 最新の値
return c;
});
}, 1000);
return () => clearInterval(id);
}, []);3. 競合状態(Race Condition)
// Bad: 競合状態が発生する可能性
useEffect(() => {
async function fetchData() {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
setUser(data); // userIdが変わった後も古いデータで更新される可能性
}
fetchData();
}, [userId]);
// Good: クリーンアップでキャンセル
useEffect(() => {
let ignore = false;
async function fetchData() {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
if (!ignore) {
setUser(data);
}
}
fetchData();
return () => {
ignore = true;
};
}, [userId]);まとめ
useEffectを使うべきとき
| ケース | 例 |
|---|---|
| 外部システムとの同期 | API呼び出し、WebSocket、ブラウザAPI |
| サブスクリプションの設定 | イベントリスナー、オブザーバー |
| 手動でのDOM操作 | サードパーティライブラリの初期化 |
useEffectを使うべきでないとき
| ケース | 代替手段 |
|---|---|
| 計算値の導出 | レンダリング中に計算 |
| propsの変化に応じたリセット | keyプロパティ |
| ユーザーイベントの処理 | イベントハンドラ |
| 1回だけの初期化 | モジュールスコープ |
チェックリスト
- クリーンアップ関数は必要か?(イベント、タイマー、接続は必須)
- 依存配列に全ての依存を含めたか?
- Strict Modeで2回実行されても問題ないか?
- 本当にuseEffectが必要か?(計算やイベントで代替できないか)
useEffectは強力なツールですが、「外部システムとの同期」という本来の目的を常に意識し、不要な使用を避けることで、より保守しやすいReactアプリケーションを構築できます。