はじめに
Reactコンポーネントは、作成から破棄までの間に様々な段階を経ます。この一連の流れを「ライフサイクル」と呼びます。本記事では、Reactコンポーネントのライフサイクルについて、初心者の方にも分かりやすく解説します。
ライフサイクルの3つのフェーズ
Reactコンポーネントのライフサイクルは、大きく3つのフェーズに分けられます。
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 作成 │ --> │ 更新 │ --> │ 破棄 │
└─────────┘ └─────────┘ └─────────┘
フェーズ1: マウント(Mounting)
コンポーネントが初めてDOMに追加される段階です。
実行されるメソッド(順番):
constructor()
getDerivedStateFromProps()
render()
componentDidMount()
フェーズ2: 更新(Updating)
propsやstateの変更により、コンポーネントが再レンダリングされる段階です。
実行されるメソッド(順番):
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
フェーズ3: アンマウント(Unmounting)
コンポーネントがDOMから削除される段階です。
実行されるメソッド:
componentWillUnmount()
モダンなReact: 関数コンポーネントとHooks
現在のReactでは、関数コンポーネントとHooksを使用することが推奨されています。以下、TypeScriptで実装例を見ていきます。
useEffectによるライフサイクル管理
import { useEffect, useState } from 'react';
interface UserProfileProps {
userId: string;
}
interface User {
id: string;
name: string;
email: string;
}
const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
// componentDidMount と componentDidUpdate に相当
useEffect(() => {
console.log('コンポーネントがマウントまたは更新されました');
// データの取得
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error('ユーザー情報の取得に失敗しました', error);
} finally {
setLoading(false);
}
};
fetchUser();
// componentWillUnmount に相当(クリーンアップ関数)
return () => {
console.log('コンポーネントがアンマウントされます');
// タイマーのクリアやイベントリスナーの削除など
};
}, [userId]); // 依存配列: userIdが変更された時のみ実行
if (loading) return <div>読み込み中...</div>;
if (!user) return <div>ユーザーが見つかりません</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
ライフサイクルメソッドとHooksの対応表
クラスコンポーネント | Hooks | 用途 |
---|---|---|
constructor |
useState |
初期状態の設定 |
componentDidMount |
useEffect(() => {}, []) |
初回レンダリング後の処理 |
componentDidUpdate |
useEffect(() => {}) |
更新後の処理 |
componentWillUnmount |
useEffect(() => { return () => {} }, []) |
クリーンアップ処理 |
shouldComponentUpdate |
React.memo , useMemo
|
レンダリングの最適化 |
ライフサイクルフローチャート
マウント時:
┌──────────────┐
│ constructor │
└──────┬───────┘
↓
┌──────────────────────────┐
│ getDerivedStateFromProps │
└──────────┬───────────────┘
↓
┌──────────────┐
│ render │
└──────┬───────┘
↓
┌───────────────────┐
│ componentDidMount │
└───────────────────┘
更新時:
┌──────────────────────────┐
│ getDerivedStateFromProps │
└──────────┬───────────────┘
↓
┌────────────────────────┐
│ shouldComponentUpdate │
└──────────┬─────────────┘
↓
┌──────────────┐
│ render │
└──────┬───────┘
↓
┌───────────────────────────┐
│ getSnapshotBeforeUpdate │
└───────────┬───────────────┘
↓
┌────────────────────┐
│ componentDidUpdate │
└────────────────────┘
アンマウント時:
┌──────────────────────┐
│ componentWillUnmount │
└──────────────────────┘
実践的な使用例
1. タイマーの管理
import { useEffect, useState } from 'react';
const Timer: React.FC = () => {
const [seconds, setSeconds] = useState<number>(0);
useEffect(() => {
// タイマーの設定
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// クリーンアップ関数でタイマーをクリア
return () => {
clearInterval(interval);
};
}, []); // 空の依存配列により、マウント時のみ実行
return <div>経過時間: {seconds}秒</div>;
};
2. イベントリスナーの管理
import { useEffect, useState } from 'react';
const WindowSize: React.FC = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
// イベントリスナーの登録
window.addEventListener('resize', handleResize);
// クリーンアップ関数でイベントリスナーを削除
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
ウィンドウサイズ: {windowSize.width} x {windowSize.height}
</div>
);
};
3. APIコールとデータフェッチング
import { useEffect, useState } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
const PostList: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('データの取得に失敗しました');
}
const data = await response.json();
// コンポーネントがアンマウントされていない場合のみ更新
if (!cancelled) {
setPosts(data);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : '予期しないエラー');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchPosts();
// クリーンアップ関数
return () => {
cancelled = true;
};
}, []);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
);
};
パフォーマンス最適化のベストプラクティス
1. 不要な再レンダリングを避ける
import { useMemo, useCallback } from 'react';
interface ExpensiveComponentProps {
data: number[];
multiplier: number;
}
const ExpensiveComponent: React.FC<ExpensiveComponentProps> = ({ data, multiplier }) => {
// 計算結果をメモ化
const processedData = useMemo(() => {
console.log('重い計算を実行中...');
return data.map(item => item * multiplier);
}, [data, multiplier]);
// 関数をメモ化
const handleClick = useCallback(() => {
console.log('クリックされました');
}, []);
return (
<div>
<button onClick={handleClick}>クリック</button>
<ul>
{processedData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
2. 条件付きエフェクトの実行
import { useEffect, useRef } from 'react';
interface ConditionalEffectProps {
shouldFetch: boolean;
query: string;
}
const ConditionalEffect: React.FC<ConditionalEffectProps> = ({ shouldFetch, query }) => {
const prevQueryRef = useRef<string>('');
useEffect(() => {
// 条件に基づいてエフェクトを実行
if (shouldFetch && query !== prevQueryRef.current) {
console.log(`検索実行: ${query}`);
// APIリクエストなどの処理
prevQueryRef.current = query;
}
}, [shouldFetch, query]);
return <div>検索クエリ: {query}</div>;
};
3. React.memoによるコンポーネントの最適化
import React, { memo } from 'react';
interface ChildComponentProps {
name: string;
age: number;
}
// メモ化されたコンポーネント
const ChildComponent = memo<ChildComponentProps>(({ name, age }) => {
console.log('ChildComponent がレンダリングされました');
return (
<div>
<p>名前: {name}</p>
<p>年齢: {age}</p>
</div>
);
});
// カスタム比較関数を使用する場合
const OptimizedChild = memo<ChildComponentProps>(
({ name, age }) => {
return (
<div>
<p>名前: {name}</p>
<p>年齢: {age}</p>
</div>
);
},
(prevProps, nextProps) => {
// trueを返すとレンダリングをスキップ
return prevProps.name === nextProps.name &&
prevProps.age === nextProps.age;
}
);
よくあるアンチパターンと解決策
アンチパターン1: 加工した値をstateに保存
// ❌ 悪い例
const BadComponent: React.FC<{ date: Date }> = ({ date }) => {
const [day, setDay] = useState(date.getDay());
// propsが変更されてもdayは更新されない
return <div>曜日: {day}</div>;
};
// ✅ 良い例
const GoodComponent: React.FC<{ date: Date }> = ({ date }) => {
// レンダリング時に計算
const day = date.getDay();
return <div>曜日: {day}</div>;
};
アンチパターン2: useEffect内での無限ループ
// ❌ 悪い例
const BadInfiniteLoop: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// countが更新されるたびに実行され、無限ループになる
setCount(count + 1);
}); // 依存配列を指定していない
return <div>{count}</div>;
};
// ✅ 良い例
const GoodExample: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// マウント時に一度だけ実行
setCount(prevCount => prevCount + 1);
}, []); // 空の依存配列
return <div>{count}</div>;
};
アンチパターン3: クリーンアップの忘れ
// ❌ 悪い例
const BadCleanup: React.FC = () => {
useEffect(() => {
const timer = setInterval(() => {
console.log('タイマー実行');
}, 1000);
// クリーンアップ関数がない!
}, []);
return <div>タイマーコンポーネント</div>;
};
// ✅ 良い例
const GoodCleanup: React.FC = () => {
useEffect(() => {
const timer = setInterval(() => {
console.log('タイマー実行');
}, 1000);
// クリーンアップ関数でタイマーをクリア
return () => {
clearInterval(timer);
};
}, []);
return <div>タイマーコンポーネント</div>;
};
useEffectの依存配列パターン
// パターン1: 空の依存配列 - マウント時のみ実行
useEffect(() => {
console.log('マウント時のみ実行');
}, []);
// パターン2: 依存配列なし - 毎回のレンダリング後に実行
useEffect(() => {
console.log('毎回実行');
});
// パターン3: 特定の値に依存 - 値が変更された時のみ実行
useEffect(() => {
console.log('valueが変更された時に実行');
}, [value]);
// パターン4: 複数の依存関係
useEffect(() => {
console.log('value1またはvalue2が変更された時に実行');
}, [value1, value2]);
カスタムHookでライフサイクルロジックの再利用
// カスタムHook: ウィンドウサイズを追跡
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;
}
// カスタムHook: ローカルストレージの同期
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 使用例
const Component: React.FC = () => {
const windowSize = useWindowSize();
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>ウィンドウサイズ: {windowSize.width} x {windowSize.height}</p>
<p>テーマ: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
テーマ切り替え
</button>
</div>
);
};
まとめ
Reactコンポーネントのライフサイクルを理解することは、効率的なアプリケーション開発に不可欠です。主なポイントは以下の通りです。
- ライフサイクルは3つのフェーズ(マウント、更新、アンマウント)で構成されます
- モダンなReactではHooksを使用して、より簡潔にライフサイクルを管理できます
- useEffectは万能で、ほとんどのライフサイクル処理をカバーできます
- クリーンアップは重要で、メモリリークを防ぐために必ず実装します
- パフォーマンス最適化には、useMemo、useCallback、React.memoを適切に使用します
- 依存配列の理解が、正しいエフェクトの実行タイミングの制御に不可欠です
- カスタムHookにより、ライフサイクルロジックを再利用可能にできます
これらの概念を理解し、適切に活用することで、保守性が高く、パフォーマンスの良いReactアプリケーションを開発できます。特に、関数コンポーネントとHooksを使用した現代的なアプローチは、コードをより簡潔で理解しやすくし、テストもしやすくなります。