3秒でわかる
useEffect や useMemo に渡す第 2 引数の配列。前回の値と比べて処理を動かすかどうかを決める、React の再実行スイッチです。
もう少し詳しく
どういうものか
useEffect、useMemo、useCallback といった React のフックに第 2 引数として渡す配列。React は再レンダリングのたびに、この配列の要素を前回の値と 1 つずつ比較する。1 つでも違っていれば中の処理を動かし、すべて同じなら飛ばす。つまり配列そのものが「いつ動くか」の条件式になっている。
比較には Object.is が使われる。数値や文字列は値そのもので一致するが、オブジェクトや配列や関数は中身ではなく参照で比べられる。この差が後述のつまずきの原因になる。
なぜ必要か
依存配列を省略すると、useEffect は毎回のレンダリング後に実行される。中で state を更新していれば再レンダリングが起き、そのたびにまた実行され、無限ループになる。逆に空配列を渡すと初回マウント時の 1 回だけになる。この 3 パターンの使い分けが、そのままフックの挙動の設計になる。
具体例
import { useEffect, useState } from "react";
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true;
};
}, [userId]); // userId が変わったときだけ取り直す
return <p>{user ? user.name : "読み込み中"}</p>;
}userId が 1 から 2 に変わった瞬間だけ fetch が走る。同じ userId のまま親が再レンダリングされても、通信は起きない。
つまずきやすいところ
一番多いのが、オブジェクトや関数をそのまま依存に入れてしまう例。
const options = { limit: 20 }; // 毎回新しい参照
useEffect(() => { load(options); }, [options]); // 毎回実行されるoptions は中身が同じでもレンダリングのたびに別のオブジェクトとして作り直されるため、比較は常に「変わった」と判定される。回避するには options を useMemo で包むか、options.limit のようにプリミティブ値を依存に置く。
もう 1 つは、警告を消したいだけで依存を削る行為。eslint の exhaustive-deps が指摘する変数を配列から抜くと、古い値を参照したまま動く「stale closure」になり、画面と実際の値がずれる。
似た用語との違い
| 書き方 | 実行タイミング |
|---|---|
| 第 2 引数なし | 毎回のレンダリング後 |
[] | マウント時の 1 回だけ |
[a, b] | a か b が変わったとき |
覚え方
「配列に書いた値が変わったら、もう一度やり直す」。書き忘れは動きすぎ、書きすぎは動かなさすぎと覚えておくと迷いにくい。