React レッスン6
useEffect
副作用の処理(API呼び出し、タイマーなど)を学ぼう
useEffectとは?
useEffectは、コンポーネントの「副作用(side effect)」を処理するためのHookです。 副作用とは、レンダリング以外の処理のことです。
- APIからデータを取得する
- タイマーをセットする
- ドキュメントのタイトルを変更する
- イベントリスナーを登録する
- ローカルストレージに保存する
useEffectはレンダリング後に実行されます。UIの描画をブロックしません。
useEffectの基本構文
import { useState, useEffect } from "react";
function DocumentTitle() {
const [count, setCount] = useState(0);
// 基本構文: useEffect(コールバック関数, 依存配列)
useEffect(() => {
// この中のコードはレンダリング後に実行される
document.title = `カウント: ${count}`;
}, [count]); // countが変わった時だけ実行
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}依存配列の3つのパターン
依存配列(第2引数)の指定方法で、実行タイミングが変わります。
import { useState, useEffect } from "react";
function EffectPatterns() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// パターン1: 依存配列なし → 毎回のレンダリング後に実行
useEffect(() => {
console.log("毎回実行される");
}); // 注意: 無限ループの原因になりやすい
// パターン2: 空の依存配列 → マウント時(初回レンダリング後)のみ
useEffect(() => {
console.log("初回のみ実行される");
// API呼び出しなどに最適
}, []);
// パターン3: 依存する値を指定 → その値が変わった時に実行
useEffect(() => {
console.log("countが変わった:", count);
}, [count]);
// 複数の依存
useEffect(() => {
console.log("countまたはnameが変わった");
}, [count, name]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}クリーンアップ関数
useEffectから関数を返すと、それがクリーンアップ関数になります。 コンポーネントがアンマウントされる時や、次のeffectが実行される前に呼ばれます。 イベントリスナーの解除やタイマーのクリアに使います。
import { useState, useEffect } from "react";
// タイマーのクリーンアップ
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// クリーンアップ: コンポーネントが消える時にタイマーを停止
return () => {
clearInterval(intervalId);
};
}, []); // 空配列 → マウント時にセット、アンマウント時にクリア
return <p>経過時間: {seconds}秒</p>;
}
// イベントリスナーのクリーンアップ
function WindowResize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
// クリーンアップ: リスナーを解除
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>画面幅: {width}px</p>;
}
// WebSocket接続のクリーンアップ
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect(); // 切断
};
}, [roomId]); // roomIdが変わったら再接続
return <div>チャットルーム: {roomId}</div>;
}クリーンアップを忘れると、メモリリークやバグの原因になります。 リスナー登録やサブスクリプションには必ずクリーンアップを書きましょう。
データフェッチング
APIからデータを取得するパターンは、useEffectの最も一般的な使い方の1つです。
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// AbortControllerでキャンセル可能にする
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch(
"https://jsonplaceholder.typicode.com/users",
{ signal: controller.signal }
);
if (!response.ok) throw new Error("取得に失敗しました");
const data = await response.json();
setUsers(data);
setError(null);
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchUsers();
// クリーンアップ: リクエストをキャンセル
return () => controller.abort();
}, []); // マウント時のみ実行
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}
// IDに基づくデータ取得
function UserDetail({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
signal: controller.signal,
})
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, [userId]); // userIdが変わるたびに再取得
if (!user) return <p>読み込み中...</p>;
return <h2>{user.name}</h2>;
}よくある間違い
// NG: useEffect内でstateを更新 → 無限ループ
function Bad() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // state更新 → 再レンダリング → effect実行 → 無限ループ!
}); // 依存配列なし
return <p>{count}</p>;
}
// NG: 依存配列にオブジェクトを入れる(毎回新しい参照)
function AlsoBad() {
const [data, setData] = useState([]);
const options = { limit: 10 }; // 毎回新しいオブジェクトが作られる
useEffect(() => {
fetchData(options);
}, [options]); // 毎回 "変わった" と判定 → 無限ループ!
// 解決策: useMemoでメモ化するか、値を直接依存に入れる
}
// OK: 正しいパターン
function Good() {
const [count, setCount] = useState(0);
// 依存配列を正しく指定
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <p>{count}</p>;
}まとめ
useEffectはレンダリング後に副作用を実行する- 依存配列で実行タイミングを制御(空 = 初回のみ、指定 = 変更時)
- クリーンアップ関数でリソースを適切に解放する
- データフェッチングではAbortControllerでキャンセル対応する
- 依存配列の指定ミスで無限ループに注意