AIに「Reactでデータ取得して」と頼むと、だいたい useEffect が返ってくる
Claude Code や ChatGPT に「ReactでAPIからデータを取ってきて表示して」と頼むと、高い確率で useEffect を使ったコードが返ってきます。動くように見えるし、それっぽい。でも未経験のうちは、この useEffect の**第2引数(依存配列 / deps)**の扱いを1文字間違えるだけで、次の二択のどちらかにハマります。
- 無限ループ: ずっと再レンダーし続けて、APIを叩き続ける
- 更新されない: state を変えたのに effect が古い値のまま動く
しかも厄介なことに、AIは deps を「空にして警告を黙らせる」か「なんでも入れて無限ループさせる」かのどちらかをやりがちです。この記事では、依存配列の読み方と、AIが書いたコードをレビューする観点を、Before → After のコードで整理します。
そもそも依存配列(deps)とは何か
useEffect(fn, deps) の第2引数が依存配列です。ルールはシンプルで、deps に挙げた値が前回のレンダーから変化したときだけ、effect(副作用の関数)が再実行される、これだけです。
// パターン1: deps を省略 → 毎レンダーで実行される
useEffect(() => {
console.log("毎回走る");
});
// パターン2: 空配列 [] → マウント時に1回だけ
useEffect(() => {
console.log("最初の1回だけ");
}, []);
// パターン3: [count] → count が変化したときだけ
useEffect(() => {
console.log("count が変わったときだけ走る:", count);
}, [count]);
ここで一番大事な注意点があります。React が「変化したかどうか」を判定するとき、deps の各要素を Object.is 相当の浅い比較(前回の要素と今回の要素を1つずつ参照で比べる) で見ています。数値や文字列などのプリミティブ値なら「値が同じか」で比べられますが、オブジェクト・配列・関数は「中身が同じか」ではなく「同じ参照(メモリ上の同じ実体)か」で比べられる、という点がすべての罠の入り口です。
罠A: 無限ループ(オブジェクトを deps に直接入れる)
まず、未経験がやりがちな無限ループから。
// ❌ Before: 無限ループする
function UserList() {
const [users, setUsers] = useState<User[]>([]);
// 毎レンダーで「新しい」オブジェクトが生成される
const options = { role: "member", limit: 20 };
useEffect(() => {
fetchUsers(options).then(setUsers); // setState する
}, [options]); // ← options は毎回別参照なので毎回「変化した」と判定される
return <List users={users} />;
}
何が起きているか、順番に追うとこうです。
- レンダーのたびに
optionsは新しいオブジェクトリテラルとして作られる(中身は同じでも参照は別物) - deps の
optionsは前回と今回で参照が違う → React は「変化した」と判定 → effect 実行 - effect 内で
setUsers→ 再レンダー - 再レンダーでまた新しい
optionsが作られる → 2 に戻る
こうして「setState → 再レンダー → deps が別参照 → effect 再実行 → setState」がぐるぐる回り続けます。オブジェクトだけでなく、配列リテラル [...] や、コンポーネント内で定義した関数も同じ理由で毎回別参照になります。
After: 参照を安定させる or プリミティブに分解する
// ✅ After その1: primitive に分解して deps に入れる
useEffect(() => {
fetchUsers({ role, limit }).then(setUsers);
}, [role, limit]); // role と limit は文字列・数値なので値で比較され、安定
// ✅ After その2: どうしてもオブジェクトを渡したいなら useMemo で参照を固定
const options = useMemo(() => ({ role: "member", limit: 20 }), []);
useEffect(() => {
fetchUsers(options).then(setUsers);
}, [options]); // options の参照は初回のみ生成され、以降は同じ実体
useMemo(() => ({...}), []) は「deps(この場合は [])が変わらない限り、同じオブジェクトの参照を返し続ける」ので、毎レンダーで別参照になる問題が消えます。関数を deps に入れたいときは同様に useCallback を使います。
罠B: 更新されない(deps 入れ忘れ = stale closure)
無限ループが怖くて、逆に「deps を空 [] にしておけば安全」と考えると、今度は反対側の罠に落ちます。
// ❌ Before: keyword を変えても検索結果が更新されない
function Search({ keyword }: { keyword: string }) {
const [results, setResults] = useState<Item[]>([]);
useEffect(() => {
search(keyword).then(setResults);
}, []); // ← keyword を使っているのに deps に入れていない
return <Results items={results} />;
}
この effect はマウント時の keyword をキャプチャ(捕まえて固定)したまま動きます。あとから props の keyword が変わっても、effect は最初の値を握りしめたまま再実行されない。これがいわゆる stale closure(古い値を閉じ込めたクロージャ) です。ユーザーからは「入力しても検索結果が変わらない」というバグに見えます。
After: effect 内で参照している値は deps に入れる
// ✅ After: keyword を deps に入れる
useEffect(() => {
search(keyword).then(setResults);
}, [keyword]); // keyword が変わるたびに再検索される
原則は「effect の中で参照している state / props は、原則すべて deps に入れる」です。これを機械的にチェックしてくれるのが、次に紹介する ESLint ルールです。
対処の指針: exhaustive-deps に従う
eslint-plugin-react-hooks の exhaustive-deps ルールは、「effect 内で使っているのに deps に入っていない値」を検出して警告してくれます。罠B(入れ忘れ)を静的に防ぐための強力な味方です。
対処をまとめるとこうなります。
- プリミティブ値(文字列・数値・真偽値)を deps に入れる — 値で比較されるので安定する
- オブジェクト・配列・関数は
useMemo/useCallbackで参照を固定してから deps に入れる -
本当にマウント時1回だけでいいなら
[]にする — ただし「なぜ空でいいのか」をコメントで残す -
exhaustive-depsの警告を安易に握りつぶさない — 警告が出たら、まず「その値は本当に deps に不要か」を疑う
exhaustive-deps の警告を // eslint-disable-next-line で消すのは、古い値を意図的に握ると決めた明確な理由があるときだけにしましょう。「よく分からないけど警告が消えたから」で無効化すると、それは罠Bを自分で仕込んでいるのと同じです。
AIが書いた useEffect をレビューする観点
AIは useEffect を「それっぽく」書きますが、deps の正しさまでは保証してくれません。生成コードを受け取ったら、最低限この3点を人間の目で確認してください。
-
deps が
[]になっている effect: 中で props / state を参照していないか? 参照しているなら罠B(更新されない)の疑い -
deps にオブジェクト / 配列リテラル / インライン関数が直接入っている: 毎レンダー別参照 → 罠A(無限ループ)の疑い。
useMemo/useCallback化を検討 -
eslint-disable exhaustive-depsが付いている: AIが警告を黙らせただけの可能性。なぜ無効化が正当なのか説明できるか?
実際にレビューを頼むなら、こんなプロンプトが有効です。
このuseEffectの依存配列(deps)をレビューして。
- depsに入れたオブジェクト/配列/関数が毎レンダー新しい参照になって
無限ループを起こしていないか
- effect内で参照しているstate/propsでdepsに入れ忘れているものがないか
(stale closureになっていないか)
- exhaustive-depsの警告を握りつぶしていないか
問題があればuseMemo/useCallback/primitive分解のどれで直すか提案して。
「書かせて終わり」ではなく、deps の1行だけは自分で読めるようになる。これだけで、AIコーディングでのReactのハマりは大きく減ります。
中上級者には当たり前すぎる内容なので、Reactを触り始めた知り合いへの紹介や、社内研修・後輩育成の参考にでもどうぞ。
未経験から Next.js + Supabase + Claude Code で Web アプリを公開するまで を、実際に手を動かしながら学べる導線を用意しています。
- 🧪 無料でお試し(trial教材・GitHub): https://github.com/ayies128/next-ai-camp-trial
- 📘 教材付きメンタリングプラン(MENTA): https://menta.work/plan/20251?ref=qiita
- 🎥 YouTube『AIエンジニア情報局』(※MENTAとは別運営の情報発信チャンネルです)
useEffect の deps みたいに「AIは書けるけど、読めないとハマる」ポイントを、手を動かしながら1つずつ潰していく構成にしています。