【解決】useEffectの無限ループ:依存を消す前に確認する5つの原因
useEffectの無限ループを、state更新と依存値の変化に分けて診断します。不要なEffect、オブジェクト参照、コールバック、親子の更新連鎖を修正し、StrictModeとの違いも確認します。
制作・運営:React Guide 運営者
useEffectが止まらないときは、依存配列を空にする前に「何の変更を、何に同期させたいか」を確認します。たとえば検索語が変わったら検索結果を取得したいのに、依存から検索語を削除すると、ループが消えても検索機能は壊れます。
この記事はReact 19系を対象に、Effectで更新したstateが、次のEffectを再実行させていないかを調べるための診断ガイドです。コードは記載した箇所の抜粋です。API呼び出しの例にはバックエンドが必要です。
2026-09-09訂正:以前の関数依存の例は、参照が安定したstate setterを渡しており、説明したループを再現しませんでした。新しい関数を渡す例へ修正しました。また「空配列なら必ず1回」「オブジェクトは依存配列に入れてはいけない」という説明を改めました。
まず「繰り返し実行」と「無限ループ」を区別する
依存配列を省略したEffectは、コミットのたびに動きます。しかし、ログを出すだけのEffectが、それだけで次のレンダーを発生させるわけではありません。
useEffect(() => {
console.log('コミット後の確認')
})ループを疑うのは、操作を止めても更新が続くときです。「Effect内のstate更新」と「その更新によって変わる依存値」を1本の矢印でつなげてみてください。親のstateを子から更新するケースでは、親子をまたいで矢印が戻ります。
開発時のStrictModeでは、初回に追加のsetup・cleanupが行われます。ログが2回出て止まるケースと、増え続けるケースは別です。StrictModeを外すことを修正のゴールにしません。
原因1:表示用の値をEffectでstateにコピーしている
商品一覧から絞り込み結果を計算するだけなら、別stateへ保存する必要がない場合があります。
// 問題の抜粋:itemsが毎回新しい配列ならEffectも毎回動く
const items = products.filter(p => p.available)
useEffect(() => {
setVisibleItems(items)
}, [items])setVisibleItemsで再レンダーされると、filterが新しい配列を作り、その配列が次のEffectを動かします。修正は、余分なstateとEffectを削除して計算結果を表示することです。
// productsを受け取るコンポーネント内
const visibleItems = products.filter(p => p.available)
// JSXではvisibleItemsを使う。コピー用のsetStateは不要。選択中IDから選択項目を探す、氏名を結合する、件数を数える、といった処理でも同じ観点を使えます。計算が遅いかどうかは別の問題なので、最初からuseMemoを必須にしません。
原因2:初期化やクリック処理をEffectに置いている
// 問題の抜粋
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1)
}, [count])最初から1を表示したいなら useState(1)、クリックで増やしたいなら onClick={() => setCount(c => c + 1)} とします。「画面が出たので1を足す」というEffectを使わなくて済みます。
更新関数に変えるだけで、すべてのループが止まるわけではありません。useEffect(() => setCount(c => c + 1)) は依存配列がないため、引き続き更新を繰り返します。[count] を残した場合も同様です。
前の値を使う更新の違いは、カートの連続更新実験で操作できます。
原因3:オブジェクトをレンダーのたびに作っている
次の2つは別のオブジェクトです。
Object.is({ category: 'books' }, { category: 'books' }) // falseconst options = { category } をコンポーネント内で作り、Effectの依存にすると、無関係な再レンダーでもEffectが実行されます。そこで新しいレスポンスをstateへ保存すると、更新がつながることがあります。
次の例は、オブジェクトの生成をEffect内へ移し、実際の入力である category に依存させます。
// import { useEffect, useState } from 'react' が必要
// /api/products は { id: string; name: string }[] を返すAPIが必要
const [products, setProducts] = useState<{ id: string; name: string }[]>([])
const [error, setError] = useState('')
useEffect(() => {
const controller = new AbortController()
let active = true
const query = new URLSearchParams({ category })
async function load() {
try {
const response = await fetch(`/api/products?${query}`, {
signal: controller.signal,
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const result = await response.json()
if (active) {
setProducts(result)
setError('')
}
} catch (cause) {
if (active) setError(String(cause))
}
}
void load()
return () => {
active = false
controller.abort()
}
}, [category])レスポンスの形式検証は省略しています。実際のアプリではAPIの契約に合わせて検証してください。画面では products と error を表示します。カテゴリを切り替えた際に古いレスポンスが新しい表示を上書きしないよう、cleanupも含めています。
オブジェクトを依存値にすること自体は合法です。外から受け取ったオブジェクトが変わったときに同期し直す必要があるなら、それを依存に含めます。
原因4:親から渡すコールバックが毎回変わる
// 問題の抜粋:親のレンダーごとに新しい関数を作る
function Parent() {
const [data, setData] = useState<unknown>(null)
return <Child onLoad={(incoming: unknown) => setData(incoming)} />
}
// Child内。/api/dataは毎回新しいオブジェクトを返す想定
useEffect(() => {
let active = true
fetch('/api/data')
.then(response => response.json())
.then(result => { if (active) onLoad(result) })
.catch(console.error)
return () => { active = false }
}, [onLoad])子がデータを渡すと親が再レンダーされ、新しいコールバックが子に届きます。子のEffectはその変更で再取得します。単にsetterへ渡すだけなら、親を <Child onLoad={setData} /> に変更できます。state setterの参照は安定しています。
変換処理がある場合はuseCallbackが選択肢ですが、その処理で使うpropsやstateを正しく依存に含めます。別の方法として、取得とstate管理を親へまとめれば、子から取得結果を報告するEffect自体をなくせます。
原因5:親子で同じ値をstateとして二重管理している
親の selectedIds を子のstateへコピーし、子のEffectで onChange([...localIds]) を呼び、親が新しい配列を保存する構成を考えます。内容が同じでも、新しい配列が親子を往復すると、同期処理が止まらなくなります。
修正するときは「選択状態の持ち主」を1か所にします。子はpropsを表示し、ユーザーのチェック操作でだけ onChange を呼びます。propsの変化を受け取るたびに変更イベントを返す必要はありません。
このケースでコールバックだけメモ化しても、配列のコピーが循環する設計は残ります。何を唯一の正しい値とするかを先に決めます。
修正した後に確認する3つの操作
- 何もしないで待つ:通信やログが増え続けないことを確認します。
- 本来の入力を変える:カテゴリなどを変えたら必要な再取得が起きるかを確認します。止まるだけでは不十分です。
- 素早く切り替える・画面から離れる:古いレスポンスが新しい結果を上書きせず、購読・通信の後片付けが働くかを確認します。
依存配列の警告を無効にして終わらせず、Effectが読む値と依存値が一致しているか確認してください。エラー名から切り分けたいときは Maximum update depth exceededの診断へ進んでください。
仕様の参照先
この記事のケース分けは、当サイトの修正例を調べるための手順です。外部データ取得には、利用中のフレームワークが提供する仕組みも検討してください。
確認してみる
依存を変えたら必要な再取得が起き、無操作なら止まることを確認する。取得コードはAPIの存在とレスポンス形式を仮定した抜粋。
同じテーマの教材
useEffectのクリーンアップ:メモリリークを防ぐ正しい実装
useEffectのクリーンアップ関数を解説します。タイマー・イベントリスナー・非同期処理のキャンセルなど、アンマウント時に必要なクリーンアップの実装方法をメモリリーク防止の観点から学べます。
useEffectの依存配列:正しい指定方法とよくある落とし穴
useEffectの依存配列を徹底解説。空配列・値の指定・オブジェクトや関数を依存配列に含めたときの問題と解決策、exhaustive-depsルールの使い方を実例付きで説明します。
useEffectの基本:外部との同期・依存配列・クリーンアップ
ページタイトルを同期する完成サンプルでuseEffectの役割を学びます。イベント処理との違い、依存配列、クリーンアップ、StrictModeでの確認方法を解説します。