On this page

Engineering6 min read

Fix React useEffect Infinite Loop — 4 Causes and Fixes

React useEffect infinite loops happen when your effect modifies a value in its own dependency array. Here are the 4 patterns that cause it and the exact fix for each — objects, functions, state, missing deps.

reactuseeffectinfinite-loopjavascripthooks

What Causes a useEffect Infinite Loop?

A useEffect infinite loop happens when the effect modifies a value that's also in its dependency array, causing the effect to re-trigger on every render.

javascript
useEffect(() => {
  setCount(count + 1)
}, [count])

This runs forever. count changes → effect runs → count changes again → effect runs → repeat until React throws a maximum update depth error.

The Error Message

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

Or in the browser console:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

Pattern 1: State Updated by the Effect Is in Its Own Deps

javascript
// ❌ Infinite loop
const [data, setData] = useState([])

useEffect(() => {
  setData([...data, newItem])  // updates data
}, [data])                      // which triggers effect again

Fix: Use functional update form — doesn't need data in deps.

javascript
// ✅
useEffect(() => {
  setData(prev => [...prev, newItem])
}, [])  // no dependency on data needed

Pattern 2: Object or Array as Dependency

javascript
// ❌ Infinite loop — new object created each render
const filters = { category: 'books', sort: 'price' }

useEffect(() => {
  fetchProducts(filters)
}, [filters])  // filters is a new reference every render

Objects and arrays are compared by reference, not value. { category: 'books' } creates a new object every render — React sees a new reference, treats it as a changed dependency, runs the effect again.

Fix 1: Move the object outside the component if it's static.

javascript
// ✅ Static — defined once, same reference always
const DEFAULT_FILTERS = { category: 'books', sort: 'price' }

function ProductList() {
  useEffect(() => {
    fetchProducts(DEFAULT_FILTERS)
  }, [])  // [] is fine — DEFAULT_FILTERS never changes
}

Fix 2: Use primitive values as dependencies instead of the whole object.

javascript
// ✅ Primitives are stable — only re-runs when actual value changes
const [category, setCategory] = useState('books')
const [sort, setSort] = useState('price')

useEffect(() => {
  fetchProducts({ category, sort })
}, [category, sort])

Fix 3: Memoize the object with useMemo.

javascript
// ✅ New reference only when inputs change
const filters = useMemo(
  () => ({ category, sort }),
  [category, sort]
)

useEffect(() => {
  fetchProducts(filters)
}, [filters])

Pattern 3: Function as Dependency

javascript
// ❌ Infinite loop — new function reference each render
function fetchData() {
  return fetch('/api/data').then(r => r.json())
}

useEffect(() => {
  fetchData().then(setData)
}, [fetchData])  // new function ref each render

Fix: Move the function inside the effect, or wrap in useCallback.

javascript
// ✅ Function inside effect — no external dep needed
useEffect(() => {
  function fetchData() {
    return fetch('/api/data').then(r => r.json())
  }
  fetchData().then(setData)
}, [])

// ✅ useCallback — stable ref, only changes when userId changes
const fetchData = useCallback(() => {
  return fetch(`/api/data?userId=${userId}`).then(r => r.json())
}, [userId])

useEffect(() => {
  fetchData().then(setData)
}, [fetchData])

Pattern 4: Missing Dependency Array

javascript
// ❌ Runs after every single render
useEffect(() => {
  document.title = `Items: ${items.length}`
})  // no dependency array

Fix: Add dependency array.

javascript
// ✅ Runs only when items.length changes
useEffect(() => {
  document.title = `Items: ${items.length}`
}, [items.length])

Note: useEffect with no dependency array is almost never intentional. Always ask "when should this run?" — "only once on mount" = [], "when X changes" = [x], "after every render" = no array (extremely rare and usually a mistake).

How to Diagnose a Loop

Add a ref counter to confirm:

javascript
const renderCount = useRef(0)

useEffect(() => {
  renderCount.current++
  console.log(`Effect ran ${renderCount.current} times`)
}, [yourDeps])

If count climbs rapidly, loop confirmed. Then log each dependency:

javascript
useEffect(() => {
  console.log('deps:', { depA, depB })
}, [depA, depB])

The dep that logs a new value every render is the culprit.

React DevTools Profiler also shows which component re-renders and how often — look for components re-rendering 50+ times per second.

Quick Reference

CauseFix
State updated by effect in its own depsUse functional state update: setState(prev => ...)
Object/array in depsUse primitives or useMemo
Function in depsMove inside effect or wrap in useCallback
No dependency arrayAdd [] or specific deps

For complex components where the loop isn't obvious — multiple effects, shared state across hooks — paste the component into DebugAI. It reads the full hook chain and identifies which effect-dep pair is cycling.

Debug faster starting today.

Free VS Code extension. 10 sessions/day. No credit card.

Install Free →

Related Posts

Engineering

What Is Root Cause Analysis in Debugging?

6 min read

Engineering

Why Does Python ImportError Happen? (And How to Fix It)

6 min read

← All posts