React useEffectとは何者なのかを理解する
Reactを書いていると必ず出会うのが useEffect です。API通信やタイマーを扱うときに使うことは何となく分かっていても、「そもそも何をする関数なのか」「なぜ必要なのか」が曖昧なまま使いがちです。この記事では、useEffect が何者なのかを、その背景にある「副作用」という考え方から整理しておきます。
例として、架空のオンライン料理教室アプリ CookMate を想定します。/lessons/:lessonId/ というURLでレッスン詳細ページを開く、という場面を使います。
そもそも「副作用」とは何か
useEffect を理解するには、先に 副作用(side effect) という言葉を押さえる必要があります。
Reactコンポーネントの本来の仕事は、たった一つです。
データ(props / state)を受け取って、画面(JSX)を返す。
この「本来の仕事」以外の処理を、まとめて 副作用 と呼びます。たとえば次のようなものです。
- API通信(サーバーからデータを取得する)
- タイマーの設定(
setTimeout/setInterval) - ブラウザのイベント登録(スクロールやリサイズの監視)
localStorageの読み書き- ページ遷移などの外部への働きかけ
これらは「画面を返す」という純粋な計算の外側で、外の世界に触れる処理です。そして、こうした副作用をコンポーネントの本体に直接書いてはいけません。
なぜ本体に直接書いてはいけないのか
コンポーネントの関数本体は、再描画(レンダリング)のたびに何度も実行されます。もしAPI通信を本体に直接書くと、こうなります。
export function LessonPage() {
const [lesson, setLesson] = useState(null);
// ❌ これはダメ
fetchLesson(lessonId).then(setLesson);
// setLesson で state が変わる → 再描画 → また fetch → また setLesson → ...
return <div>{lesson?.title}</div>;
}
fetch → setLesson で state が変わり、再描画され、また fetch が走り……と無限ループになってしまいます。
副作用は「描画のたび」ではなく「必要なタイミングだけ」実行したい。そのための仕組みが useEffect です。
useEffectの基本
useEffect は、描画が終わった後に副作用を実行するための関数です。基本形はこうです。
useEffect(() => {
// ここに副作用を書く(描画の後に実行される)
}, [依存する値]);
引数は2つあります。
- 実行したい処理(第1引数の関数)
- 依存配列(第2引数の
[])… 「この中の値が変わったときだけ実行する」というトリガー指定
依存配列で実行タイミングを決める
useEffect の挙動は、第2引数の依存配列で決まります。ここが最重要ポイントです。
| 書き方 | いつ実行されるか |
|---|---|
useEffect(fn, []) |
最初の1回だけ(マウント時) |
useEffect(fn, [a, b]) |
最初 + a か b が変わるたび |
useEffect(fn)(配列を省略) |
毎回の描画後(基本的に使わない) |
「最初の1回だけ実行したい」なら空配列 []、「特定の値が変わったら実行し直したい」なら、その値を配列に入れる、と覚えておけば十分です。
実例:レッスン情報を取得する
CookMateのレッスン詳細ページで、URLの lessonId をもとにレッスン情報を取得してみます。
export function LessonPage({ lessonId }: { lessonId: string }) {
const [lesson, setLesson] = useState<Lesson | null>(null);
useEffect(() => {
fetchLesson(lessonId).then(setLesson);
}, [lessonId]); // lessonId が変わったら取得し直す
if (!lesson) return <p>読み込み中...</p>;
return <h1>{lesson.title}</h1>;
}
このコードはこう読みます。
「
lessonIdが決まった(あるいは変わった)タイミングで、レッスン情報を取得する」
- ページが表示されたとき、
lessonIdを使って一度取得する - ユーザーが別のレッスン(別の
lessonId)へ移動したら、もう一度取得し直す - 取得中はまだ
lessonがnullなので「読み込み中…」を表示し、データが届いたら再描画されてタイトルが出る
この「まず画面を出す → 裏でデータを取る → 届いたら再描画」という流れこそ、useEffect が副作用を「描画の後」に実行してくれるおかげで成り立っています。
描画(まだデータ無し → 「読み込み中...」)
↓ useEffect 発火
API通信
↓ データ到着(setLesson)
再描画(タイトルを表示)
クリーンアップ:後片付けの仕組み
副作用の中には、後片付けが必要なものがあります。タイマーやイベント登録は、放置するとコンポーネントが消えた後も動き続け、メモリリークの原因になります。
useEffect の第1引数の関数から関数を return すると、それが「後片付け(クリーンアップ)」として実行されます。
useEffect(() => {
const timer = setInterval(() => {
console.log('1秒ごとに実行');
}, 1000);
// return した関数が後片付けとして呼ばれる
return () => clearInterval(timer);
}, []);
クリーンアップが呼ばれるのは、次のタイミングです。
- コンポーネントが画面から消える(アンマウント)とき
- 依存配列の値が変わって、副作用を再実行する直前(前回の後片付け)
API通信だけなら不要なことが多いですが、タイマー・購読・イベントリスナーを扱うときは必ず後片付けをセットで書く、と覚えておくと安全です。
使うときの注意点
1. 依存配列を正しく書く
useEffect の中で使っている値(state、props、関数など)は、原則すべて依存配列に入れます。入れ忘れると「古い値を参照し続ける」バグの温床になります。ReactのESLintルール(react-hooks/exhaustive-deps)が警告してくれるので、基本はそれに従うのが安全です。
2. なんでも useEffect にしない
「props から計算できるだけの値」に useEffect は不要です。副作用ではなく、ただの計算だからです。
// ❌ 不要な useEffect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 描画時に計算するだけでよい
const fullName = `${firstName} ${lastName}`;
useEffect は「外の世界に触れる処理(副作用)」のためのものであって、値の計算のためのものではありません。ここを混同すると、無駄な再描画や複雑なコードにつながります。
3. 描画の後に実行される、を忘れない
useEffect の中身は、コンポーネントが一度描画された後に実行されます。そのため「最初の描画時点ではまだデータが無い」状態が必ず存在します。前述の「読み込み中…」のような初期表示を用意しておくのが定石です。
まとめ
- Reactコンポーネントの本来の仕事は「データを受け取って画面を返す」こと。それ以外の処理が 副作用
- API通信・タイマー・イベント登録などの副作用を本体に直接書くと、無限ループなどの事故になる
useEffectは、副作用を 描画の後に・必要なタイミングだけ 実行するための関数- 依存配列 で実行タイミングを制御する(
[]なら最初の1回だけ) - タイマーや購読は、
returnする関数で クリーンアップ する - props から計算できる値に
useEffectは不要。副作用のためだけに使う
useEffect は「描画」と「副作用」を切り分けるための仕組みだと捉えると、いつ使うべきかが見えてきます。まずは「これは画面を返す仕事か、それとも外の世界に触れる仕事か」を意識するところから始めるとよいです。