はじめに
前回の導入編では、Reactの基本的な概念を学びました。今回はコンポーネントのライフサイクルについて深掘りしていきます。
ライフサイクルを理解することで、以下のようなことができるようになります:
- コンポーネントが表示された時にタイマーやイベントリスナーをセットアップする
- 画面を離れる時に、タイマーやイベントリスナーを適切に解除する
- propsやstateの変更に応じて、外部システムとの同期を取る
コンポーネントのライフサイクルとは
ライフサイクルとは、コンポーネントが生まれてから消えるまでの一連の流れのことです。
人間の一生に例えると:
- 誕生(マウント): コンポーネントが画面に表示される
- 成長(更新): propsやstateが変わって再レンダリングされる
- 死(アンマウント): コンポーネントが画面から消える
function UserProfile() {
// 1. 誕生:初回レンダリング時
console.log('コンポーネントが生まれました');
// 2. 成長:stateやpropsが変わると再レンダリング
const [count, setCount] = useState(0);
// 3. 死:画面から消える時(後で説明)
return <div>ユーザープロフィール</div>;
}クラスコンポーネント時代のライフサイクル(参考)
React Hooksが登場する前は、クラスコンポーネントでライフサイクルメソッドを使っていました。
// 古い書き方(クラスコンポーネント)
class ChatRoom extends React.Component {
// マウント時:画面に表示された直後
componentDidMount() {
console.log('コンポーネントが表示されました');
// WebSocket接続を開始
this.ws = new WebSocket(`wss://chat.example.com/${this.props.roomId}`);
}
// 更新時:propsやstateが変わった後
componentDidUpdate(prevProps) {
if (this.props.roomId !== prevProps.roomId) {
// roomIdが変わったらWebSocketを再接続
this.ws.close();
this.ws = new WebSocket(`wss://chat.example.com/${this.props.roomId}`);
}
}
// アンマウント時:画面から消える直前
componentWillUnmount() {
console.log('コンポーネントが消えます');
this.ws.close(); // WebSocket接続を閉じる
}
render() {
return <div>チャットルーム</div>;
}
}問題点:
- 複雑で覚えることが多い
- 関連するロジックがバラバラに散らばる(接続・再接続・切断が3つのメソッドに分散)
- コードが冗長になりがち
現代の方法:useEffect Hook
React 16.8以降、関数コンポーネント + Hooksが主流になりました。ライフサイクルはuseEffectフックで管理します。
useEffectの本来の目的
useEffectの目的は「外部システムとの同期」です。 Reactの外にある何かと、コンポーネントの状態を同期させるために使います。
import { useEffect, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// useEffect: 外部システム(ブラウザのタイマーAPI)との同期
useEffect(() => {
const timerId = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// クリーンアップ:タイマーを解除
return () => clearInterval(timerId);
}, []); // マウント時にセットアップ、アンマウント時にクリーンアップ
return <div>{seconds}秒経過</div>;
}「外部システムとの同期」とは具体的に何か?
Reactが管理していない外の世界とやり取りすることです:
- タイマー:
setInterval、setTimeoutのセットアップと解除 - イベントリスナー:
window.addEventListenerの登録と解除 - WebSocket: リアルタイム通信の接続管理
- DOM API:
IntersectionObserver、MutationObserverなどの監視 - ブラウザAPI:
document.titleの更新、localStorageへのアクセス
依存配列:useEffectの実行タイミングを制御する
useEffectの第2引数(依存配列)で、いつ実行するかを制御できます。
パターン1: 依存配列なし(毎回実行)
useEffect(() => {
console.log('毎回実行されます');
// レンダリングのたびに実行される
});注意: これはほとんど使いません。無限ループのリスクがあります。
// ❌ 無限ループの例
function BadExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // stateを更新
// → 再レンダリング → useEffectが実行 → stateを更新 → ...(無限ループ)
}); // 依存配列がない!
return <div>{count}</div>;
}パターン2: 空の依存配列(マウント時のみ)
useEffect(() => {
console.log('初回マウント時のみ実行');
// ウィンドウのリサイズを監視
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // ← 空の配列使い所:
- イベントリスナーの登録(1回だけ)
- タイマーのセットアップ
- WebSocket接続の確立
パターン3: 特定の値の変更を監視
function PageTitle({ title }) {
useEffect(() => {
// ブラウザのタブタイトルをpropsに同期
document.title = title;
}, [title]); // ← titleが変わった時だけ実行
return <h1>{title}</h1>;
}function ChatRoom({ roomId }) {
useEffect(() => {
// roomIdに合わせてWebSocket接続を切り替え
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (event) => console.log(event.data);
return () => ws.close(); // 前の接続を閉じる
}, [roomId]); // ← roomIdが変わった時だけ再接続
return <div>ルーム: {roomId}</div>;
}使い所:
- propsやstateの変更に応じて外部システムを再同期
- チャットルームIDが変わったら再接続
- 表示テーマが変わったらDOM属性を更新
クリーンアップ処理:後片付けの重要性
コンポーネントが消える時や、次の副作用が実行される前にクリーンアップ処理を実行できます。
クリーンアップが必要な例
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log('タイマー開始');
// 1秒ごとにカウントアップ
const timerId = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// クリーンアップ関数を返す
return () => {
console.log('タイマー停止');
clearInterval(timerId); // タイマーを解除
};
}, []); // マウント時のみ
return <div>{seconds}秒経過</div>;
}クリーンアップしないとどうなる?
// ❌ クリーンアップなし(メモリリーク)
function BadTimer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// クリーンアップがない!
// → コンポーネントが消えてもタイマーが動き続ける
// → メモリリーク、エラーの原因
}, []);
return <div>{seconds}秒経過</div>;
}クリーンアップが必要なケース
1. イベントリスナー
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
// イベントリスナーを登録
window.addEventListener('resize', handleResize);
// クリーンアップ:リスナーを解除
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>画面幅: {width}px</div>;
}2. WebSocket接続
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// WebSocket接続を開始
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (event) => {
setMessages(m => [...m, event.data]);
};
// クリーンアップ:接続を閉じる
return () => {
ws.close();
};
}, [roomId]); // roomIdが変わったら再接続
return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>;
}3. DOM監視(IntersectionObserver)
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect(); // 一度表示されたら監視を停止
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
// クリーンアップ:監視を解除
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{isVisible ? <img src={src} alt={alt} /> : <div>読み込み中...</div>}
</div>
);
}4. APIリクエストのキャンセル(AbortController)
useEffectでfetchを使う場面がゼロにはならないため、キャンセルパターンは知っておく価値があります。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
}
});
// クリーンアップ:リクエストをキャンセル
return () => {
controller.abort();
};
}, [userId]);
return <div>{user?.name}</div>;
}useEffectでのデータ取得は非推奨
かつてはuseEffect内でAPIデータを取得するのが一般的なパターンでした。しかし、現在のReactエコシステムではこのアプローチには多くの問題があるとされています。
// ⚠️ かつての一般的なパターン(現在は非推奨)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);何が問題なのか:
- ローディング状態の管理が煩雑:
loading、error、dataを手動で管理する必要がある - レースコンディション: 複数のリクエストが競合した時の制御が難しい
- キャッシュがない: 同じデータを何度も取得してしまう
- サーバーサイドレンダリングに対応できない: SSR時にウォーターフォールが発生する
現在の推奨アプローチ:
| 方法 | 特徴 |
|---|---|
| TanStack Query | キャッシュ、再取得、エラーハンドリングを自動管理 |
| SWR | 軽量なデータフェッチングライブラリ |
| Server Components | サーバー側でデータ取得(Next.js等) |
React 19では
use()フックが導入され、Promiseを直接扱える新しい方法も登場しています。詳しくはHooks応用編で扱います。
覚えておくべきこと: useEffectの目的は「外部システムとの同期」です。データの取得は同期ではなく「イベント」(画面を開いた、ボタンを押した)に対する応答であり、専用のデータフェッチングツールを使うのが適切です。
実践例:TanStack Queryを使ったユーザー検索
useEffectの使い方と、データ取得の現代的なアプローチを組み合わせた実践的な例です。
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
function UserSearch() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// useEffect: デバウンスの実装(タイマーAPIとの同期)
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timerId);
}, [query]);
// データ取得はTanStack Queryに任せる
const { data: results = [], isLoading, error } = useQuery({
queryKey: ['users', debouncedQuery],
queryFn: async () => {
const res = await fetch(`/api/search?q=${debouncedQuery}`);
return res.json();
},
enabled: debouncedQuery.length > 0, // 空文字の時は実行しない
});
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="ユーザー名を入力"
/>
{isLoading && <p>検索中...</p>}
{error && <p style={{ color: 'red' }}>検索に失敗しました</p>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}ポイント: useEffectはデバウンス(タイマーとの同期)だけに使い、データ取得はTanStack Queryに任せています。これにより、キャッシュ・エラーハンドリング・ローディング状態が自動で管理されます。
よくある間違いと対処法
間違い1: 依存配列を正しく指定していない
// ❌ 間違い:countが依存配列にない
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
console.log(count); // 常に0が表示される
setCount(count + 1); // 常に0 + 1 = 1になる
}, 1000);
return () => clearInterval(timerId);
}, []); // countが依存配列にない!
return <div>{count}</div>;
}
// ✅ 正解1:関数型の更新を使う
useEffect(() => {
const timerId = setInterval(() => {
setCount(c => c + 1); // 前の値を元に更新
}, 1000);
return () => clearInterval(timerId);
}, []); // 依存なし
// ✅ 正解2:依存配列に追加(ただしタイマーが再生成される)
useEffect(() => {
const timerId = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timerId);
}, [count]); // countを依存配列に追加間違い2: useEffect内でasync/awaitを直接使う
// ❌ 間違い:useEffectをasyncにできない
useEffect(async () => {
const data = await fetch('/api/user');
// エラー!useEffectはクリーンアップ関数を返す必要がある
}, []);
// ✅ 正解:内部でasync関数を定義
useEffect(() => {
const fetchUser = async () => {
const res = await fetch('/api/user');
const data = await res.json();
setUser(data);
};
fetchUser();
}, []);※ ただし前述の通り、データ取得にはTanStack QueryやSWRの使用を推奨します。
間違い3: useEffectを条件分岐の中に書く
// ❌ 間違い:Hooksは常に同じ順序で呼ばれる必要がある
function UserProfile({ isLoggedIn }) {
if (isLoggedIn) {
useEffect(() => {
// これはNG!
}, []);
}
}
// ✅ 正解:useEffect内で条件分岐
function UserProfile({ isLoggedIn }) {
useEffect(() => {
if (isLoggedIn) {
// 処理
}
}, [isLoggedIn]);
}useEffectの実行順序
複数のuseEffectがある場合の実行順序を理解しましょう。
function LifecycleDemo() {
const [count, setCount] = useState(0);
console.log('1. レンダリング');
useEffect(() => {
console.log('3. useEffect(依存なし)');
});
useEffect(() => {
console.log('4. useEffect(空の依存配列)- マウント時のみ');
}, []);
useEffect(() => {
console.log(`5. useEffect(count = ${count})`);
}, [count]);
console.log('2. レンダリング終了前');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}
// 初回マウント時の出力:
// 1. レンダリング
// 2. レンダリング終了前
// 3. useEffect(依存なし)
// 4. useEffect(空の依存配列)- マウント時のみ
// 5. useEffect(count = 0)
// ボタンクリック後:
// 1. レンダリング
// 2. レンダリング終了前
// 3. useEffect(依存なし)
// 5. useEffect(count = 1)ポイント:
- useEffectはレンダリング後に実行される
- 空の依存配列のuseEffectは初回のみ
- 依存配列に値があるuseEffectはその値が変わった時のみ
まとめ
Reactのライフサイクル管理はuseEffectで行います。最も大切なのは、**useEffectの目的は「外部システムとの同期」**だということです。
- 基本構文:
useEffect(() => { /* 副作用 */ }, [依存配列]) - 実行タイミング:
[]- マウント時のみ[value]- valueが変わった時- 依存配列なし - 毎レンダリング(通常使わない)
- クリーンアップ:
return () => { /* 後片付け */ } - useEffectに適した用途:
- タイマーのセットアップと解除
- イベントリスナーの登録と解除
- WebSocket接続の管理
- DOM監視(IntersectionObserver等)
- ドキュメントタイトルの更新
- データ取得にはuseEffectを使わない: TanStack Query、SWR、Server Componentsを使いましょう
次回予告: 次の「Hooks基礎編」では、useState、useReducer、useContext、useRefなど、実務で必須のHooksを体系的に学びます。useEffectで学んだ知識をベースに、Reactの状態管理をマスターしましょう!
学習の順序について: 次にHooks基礎編を学んだ後、TypeScript編に進むことをお勧めします。アーキテクチャ編は実際にコードを書いた経験を積んでから学ぶと、より理解が深まります。
参考リンク



