useEffect
の依存配列は、「いつ useEffect の中の処理を実行するか」を React に伝えるための重要な仕組みです。
基本構文
useEffect(() => {
// 副作用の処理(例:API 呼び出し、イベント)
}, [依存変数1, 依存変数2, ...]);
依存配列の意味
依存配列の書き方 | 実行されるタイミング |
---|---|
[] (空配列) |
初回マウント時のみ。1回だけ実行される。 |
[依存変数1, 依存変数2] |
依存変数1 または 依存変数2 が 変化したとき に実行される。 |
書かない(省略) | 毎回レンダリング後に実行される。非推奨。 |
依存配列が必要な場面
① API呼び出しなどの非同期処理
例: 特定のパラメータが変わったときに API を再取得したい。
useEffect(() => {
fetch(`/api/data?query=${query}`).then(...);
}, [query]); // query が変わるたびに再実行
ポイント
-
query
が変わったときだけリクエストしたい。 - 依存配列がないと、毎回レンダリングでリクエストしてしまいパフォーマンス悪化。
② イベントリスナーの登録・解除
例: ある条件でスクロールイベントを登録したい。
useEffect(() => {
const onScroll = () => console.log("scrolled!");
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [enabled]); // enabled の変化に応じてイベントを切り替え
ポイント
- 状態に応じてイベント登録を切り替える必要がある。
- 依存配列がないと、意図しないタイミングでイベントが追加・削除されない。
③ setInterval / setTimeout の使用
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]);
ポイント
-
count
が変わるたびに、新しいクロージャに基づいて動作してほしい。 - 依存配列がなければ、古い
count
をずっと使ってしまう。
④ 親から渡された props の変化を監視したいとき
useEffect(() => {
console.log("親からの user が変わった", user);
}, [user]);
ポイント
- props の変化をトリガーに何かをしたい(ログを出す、状態を更新する、など)。
なぜ依存配列が重要か
1. useEffect
は副作用の実行タイミングを制御する仕組み
React のコンポーネントは何度も再レンダリングされますが、副作用(例:API通信、DOM操作、イベント登録)はそのたびに無条件で行うと無駄やバグの原因になります。
そのため、 必要なときだけ副作用を実行する必要があり、その条件を指定するのが「依存配列」です。
2. 依存配列 = 「この値が変わったら再実行していいよ」リスト
React は依存配列を監視して、
- 変化があれば
useEffect
を再実行 - 変化がなければスキップ
という最適化を行います。
依存配列を省略するとどうなるか
useEffect(() => {
// 毎回実行される
doSomething();
});
デメリット 1:パフォーマンスの悪化
- レンダリングごとに副作用を実行してしまい、不要な処理が増える
- たとえば API リクエストを毎回送る → 無駄な通信・サーバー負荷増
デメリット 2:状態のループ・無限再実行のリスク
useEffect(() => {
setValue("abc");
});
-
setValue
→ 再レンダリング →useEffect
→ またsetValue
... - 無限ループが発生する可能性あり(特に依存関係を持つ場合)
デメリット 3:古い値のまま処理される
useEffect(() => {
console.log(count); // 最新の count を見てない可能性がある
}, []); // count に依存しているのに空配列にしてしまった
- 本来
count
が変わるたびに実行されるべき処理が、 - 空配列により「初回しか動かない」=状態が変わっても処理されない
難しい論点と設計ポイント
1. 関数・オブジェクト・配列の参照変化問題
問題
依存配列に関数やオブジェクト・配列など「毎回新しく生成される値」を入れると、毎回再実行される。
useEffect(() => {
doSomething();
}, [someFunc]); // someFunc は useEffect 外で毎回新しく定義されてると NG
解決
-
useCallback
/useMemo
でメモ化して、参照が変わらないようにする。
const stableFunc = useCallback(() => {
...
}, [deps]);
useEffect(() => {
stableFunc();
}, [stableFunc]);
- React は浅い比較しかしないので、再実行を避けるには同一参照を保つ工夫が必要。
2. useEffect のクロージャトラップ
問題
依存配列に入れていない変数の古い値を useEffect
がキャプチャして使ってしまう。
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// この中の count は「この useEffect が定義された時点の count」
console.log(count);
}, []);
}
上記の useEffect
の中の count
は、初回の count=0
のときのスナップショットを「クロージャとしてキャプチャ」しています。
つまり、useEffect
の中だからといって、自動的に最新の値が使われるとは限りません。
解決
- 最新の値を使いたい場合は
ref
を使う。
const countRef = useRef(count);
// count が変わるたびに ref を更新
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // 毎回最新の count
}, 1000);
return () => clearInterval(id);
}, []); // setInterval 自体は1回だけでOK
3. 副作用のキャンセルとクリーンアップ
シーン
非同期通信やタイマーを使っていて、コンポーネントのアンマウント時や再実行時に前の処理を無効化したい。
useEffect(() => {
let isMounted = true;
fetch("/api").then((res) => {
if (isMounted) setData(res);
});
return () => {
isMounted = false;
};
}, []);
または AbortController を使う
useEffect(() => {
const controller = new AbortController();
fetch("/api", { signal: controller.signal }).catch((e) => {
if (e.name !== "AbortError") throw e;
});
return () => controller.abort();
}, []);
4. 再実行を意図的に抑制・強制したいケース
条件によって実行したりしなかったりするような制御も可能
useEffect(() => {
if (!shouldRun) return;
doSomething();
}, [shouldRun, triggerKey]);
→ 外部の triggerKey
などをトグルして意図的に再実行させる設計。
useEffect の依存配列まとめ
-
useEffect
は副作用を定義するフックで、いつ実行するかを依存配列で制御する。 - 依存配列に値を入れると、その値が変わったときだけ実行される。
- 省略すると毎回実行されるため、パフォーマンス悪化や無限ループの原因になる。
- オブジェクト・配列・関数は参照が毎回変わるので、
useMemo
やuseCallback
でメモ化して安定させる。 - 最新の状態を非同期内などで参照したい場合は、
useRef
で最新値を保持するのが安全。