useEffect — 副作用と外部同期
useEffect は、DOMの更新が反映されたあとに実行される処理を書く場所です。
API呼び出し、購読の開始・停止、タイマー、ブラウザAPIの操作などが典型的な用途です。
学習者useEffect ってとりあえず何でも入れちゃってるけど、本当はどういう処理を書く場所なの?
useEffectの役割 — 外部との同期
useEffect は「副作用」と呼ばれますが、より正確には 「レンダー結果を外部と同期するための仕組み」 です。
「外部」とは、Reactが管理していないものすべてを指します。サーバーAPI、ブラウザのタイマー、localStorageやsessionStorage、DOM要素のサイズ・スクロール位置、WebSocket接続、サードパーティライブラリなどです。
import { useEffect, useState } from 'react';
export function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(id); // クリーンアップ
}, []);
return <p>{now.toLocaleTimeString()}</p>;
}この例では、ブラウザのタイマー(外部)とReactのstate(内部)を同期しています。
レンダーとマウントの違い
useEffect の依存配列を理解するには、レンダー・マウント・アンマウントの違いを押さえておく必要があります。詳しくは「レンダーとマウント — コンポーネントのライフサイクル入門」で解説しています。
要点だけ整理すると:
- マウント — コンポーネントが最初にDOMに追加されること。ライフサイクル中に1回だけ起きます。
- レンダー — コンポーネントの関数が呼ばれてUIを計算すること。stateやpropsが変わるたびに何度でも起きます。
- アンマウント — コンポーネントがDOMから取り除かれること。
useEffect(..., []) が「マウント時のみ」と言われるのは、空配列だと依存する値がないため「何にも反応しない = 最初の1回しか実行されない」ためです。
依存配列の3パターン
第2引数の依存配列は「このエフェクトが何に反応するか」の宣言です。
依存配列なしと空配列 [] は変わるか
変わります。 見た目は似ていますが、実行タイミングがまったく異なります。
// 毎レンダー後に実行
useEffect(() => {
console.log('rendered');
});
// マウント時のみ
useEffect(() => {
console.log('mounted');
}, []);| 書き方 | 実行タイミング |
|---|---|
| 依存配列なし | 毎レンダー後 |
[](空配列) | マウント時に1回だけ |
[value] | マウント時 + value が変わるたび |
[] — マウント時に1回だけ実行
useEffect(() => {
// コンポーネントがマウントされたときに1回だけ実行
const id = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(id); // アンマウント時にクリーンアップ
}, []);空配列は「依存する値がない=何にも反応しない」という意味です。初期化処理や、一度だけ立ち上げて解除する購読に使います。
[value] — 特定の値が変わったら再実行
useEffect(() => {
fetchUserProfile(userId).then(setProfile);
}, [userId]);userId が変わるたびにエフェクトが再実行されます。前回のクリーンアップが先に走ってから、新しいエフェクトが実行されます。
省略 — 毎レンダー後に実行
useEffect(() => {
console.log('毎レンダー後に実行される');
});依存配列を省略すると、あらゆるstate・propsの変化のたびに再実行されます。空配列 [] とは異なり、再レンダーのたびにエフェクトが走ります。意図しない無限ループの温床になるため、基本的に使いません。
クリーンアップ関数の実行タイミング
useEffect から返す関数(クリーンアップ)は、2つのタイミングで実行されます。
useEffect(() => {
console.log(`接続開始: userId=${userId}`);
return () => {
console.log(`接続解除: userId=${userId}`);
};
}, [userId]);1. 依存値が変わって再実行される前
エフェクトが再実行されるとき、まず前回のクリーンアップが走り、そのあとに新しいエフェクトが実行されます。
// userIdが "alice" → "bob" に変わったとき
接続解除: userId=alice ← 前回のクリーンアップ
接続開始: userId=bob ← 新しいエフェクト
2. コンポーネントがアンマウントされるとき
コンポーネントが画面から取り除かれるとき、最後のクリーンアップが実行されます。
クリーンアップを忘れると、タイマーやイベントリスナーがコンポーネント消滅後も動き続ける「メモリリーク」が起きます。特に setInterval と addEventListener は忘れやすいので注意してください。
イベントハンドラとの使い分け
useEffect に書くべきかイベントハンドラに書くべきかの判断は、「その処理はレンダーの結果として必要か、それともユーザーの操作に対する応答か」で決まります。
イベントハンドラで十分なケース
// ボタンを押したらAPIを呼ぶ → イベントハンドラでよい
function SubmitButton() {
const handleClick = async () => {
await fetch('/api/submit', { method: 'POST' });
};
return <button onClick={handleClick}>送信</button>;
}ユーザーが「送信」ボタンを押したから実行する処理です。レンダーとは関係ないので useEffect は不要です。
useEffectが必要なケース
// ページが表示されたらデータを取得する → レンダーの結果として必要
function UserProfile({ userId }: { userId: string }) {
const [profile, setProfile] = useState<Profile | null>(null);
useEffect(() => {
fetchUserProfile(userId).then(setProfile);
}, [userId]);
return profile ? <p>{profile.name}</p> : <p>読み込み中...</p>;
}「このコンポーネントが表示されているなら、対応するデータが必要」という同期関係があります。これは useEffect の出番です。
不要なuseEffectの典型例
// NG — propsやstateから計算できる値をuseEffectで同期している
function FilteredList({ items, query }: { items: Item[]; query: string }) {
const [filtered, setFiltered] = useState<Item[]>([]);
useEffect(() => {
setFiltered(items.filter((item) => item.name.includes(query)));
}, [items, query]);
return <ul>{filtered.map(/* ... */)}</ul>;
}
// OK — レンダー中に直接計算する
function FilteredList({ items, query }: { items: Item[]; query: string }) {
const filtered = items.filter((item) => item.name.includes(query));
return <ul>{filtered.map(/* ... */)}</ul>;
}propsやstateから導出できる値は、レンダー中に計算すれば済みます。useEffect で別のstateに入れ直すのは、不要な再レンダーを生むだけです(計算コストが高い場合は次章以降の useMemo を使います)。
無限ループの典型パターン
学習者useEffect を書いたら、ブラウザが固まって無限ループした…!何が起きてるの?
「エフェクトの中でstateを更新 → 再レンダー → またエフェクトが走る」が止まらなくなるのが典型的な原因です。2つのパターンで見てみましょう。
パターン1: 依存配列に含めた値をエフェクト内で更新する
// NG — countが変わる → エフェクト実行 → countを更新 → また実行…
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 無限ループ!
}, [count]);パターン2: 依存配列を省略してsetStateしている
// NG — 毎レンダー後に実行 → stateを更新 → 再レンダー → また実行…
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}); // 依存配列がない → 毎回実行依存配列の省略は [] と書き忘れている場合がほとんどです。ESLintの警告を無視せずに対処することが重要で、これは次の章で詳しく扱います。
まとめ
useEffect はレンダー結果を外部と同期するためのフックです。API呼び出し、タイマー、購読など、Reactの外側にあるものとの接続に使います。
依存配列はエフェクトの再実行条件を宣言するもので、省略すると毎レンダー後に実行され、[] ならマウント時に1回だけ、[value] なら値の変化時に再実行されます。省略はほぼ書き忘れかバグと考えてよいです。
クリーンアップ関数は「再実行前」と「アンマウント時」の2タイミングで走ります。タイマーやイベントリスナーの解除を忘れないでください。
イベントハンドラで済む処理を useEffect に書くのは不要なuseEffectの典型です。「ユーザーの操作への応答」はイベントハンドラ、「レンダー結果に紐づく外部同期」はuseEffectと覚えてください。