LEARN · DEBUGGING GUIDE

Diagnosing useEffect Infinite Loops in React Applications

Infinite loops from useEffect can cripple your React app fast. Here’s how to pinpoint the culprit, stabilize your dependencies, and regain control.

IntermediateReact bugs4 min read

What this usually means

You have declared a useEffect hook with a dependency array that changes on every render, causing React to trigger the effect repeatedly. The most common beginner mistake is including objects, arrays, or function references as dependencies, which are recreated every render unless memoized. Sometimes, updating state within the effect, which itself is a dependency, also causes this feedback loop. Even experienced developers trip over custom hooks or selectors whose outputs are unstable.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Open Chrome DevTools Console—look for 'Maximum update depth exceeded' or similar errors.
  • 2Locate the component causing the spike using React DevTools Profiler or the Components tab.
  • 3Add console.log statements inside the suspicious useEffect to confirm how often it fires and what dependencies have changed.
  • 4Review the dependency array: are you including non-primitive values (objects, arrays, functions)?
  • 5Temporarily comment out the useEffect body—does the infinite loop stop?
  • 6Check if you are setting local state inside the useEffect with that same state in its dependency array.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe React component source file where the problematic useEffect is defined
  • searchAll custom hooks called inside that component (e.g., 'useMySelector', 'usePrevious')
  • searchAny selectors or memoized computations (e.g., reselect selectors) that are used as dependencies
  • searchReact DevTools Profiler timeline for component re-renders
  • searchNetwork panel in Chrome DevTools for repeated API calls
  • searchThe dependency array of the offending useEffect—be exact about the values
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningIncluding a freshly-created object or array (e.g., { foo }) in the dependency array without useMemo
  • warningPassing an inline function to useEffect and listing it as a dependency
  • warningUpdating state inside useEffect that is also in the dependency array
  • warningIncluding unstable values from props or selectors that change every render
  • warningFailing to memoize event handlers or context values passed as dependencies
  • warningMutating dependencies outside of React state (e.g., window object or refs triggering indirect changes)
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildWrap objects and arrays in useMemo so their reference stays stable: useMemo(() => ({foo}), [foo])
  • buildDefine callback functions outside the component or wrap with useCallback
  • buildRemove state setters from the dependency array when safe (ensure no stale closures)
  • buildValidate that custom hooks or selectors return memoized, stable values
  • buildDouble-check your state update logic—don’t re-trigger on every effect run
  • buildIf using Redux or MobX, prefer selectors that are referentially stable
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedReload the page—CPU usage should remain normal and the UI must stay responsive
  • verifiedConfirm the useEffect fires only when expected by logging dependency values
  • verifiedObserve the Profiler timeline—the component should not re-render indefinitely
  • verifiedNetwork tab should show API calls only when dependencies actually change
  • verifiedDo a hot reload—confirm no runaway effects or crash loops are reintroduced
  • verifiedAdd unit or integration tests to cover edge-case dependency changes
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningBlindly omitting dependencies to 'fix' the loop—leads to stale reads and hidden bugs
  • warningUsing JSON.stringify in the dependency array to 'force' shallow equality—destroys performance
  • warningIgnoring warnings from eslint-plugin-react-hooks
  • warningLeaving unnecessary objects/arrays/functions in the dependency array
  • warningForgetting to memoize returned values from selectors or custom hooks
  • warningAssuming that ref values in dependencies are always stable (they’re not unless explicitly managed)
( 07 )War story

Endless API Fetch Calls from useEffect Dependency Trap

Frontend EngineerReact 17, Redux Toolkit, Chrome DevTools, custom selector hooks

Timeline

  1. 13:00Deployed new dashboard with an API fetch in useEffect.
  2. 13:03PagerDuty alert for CPU spike on client machines.
  3. 13:05Checked Chrome DevTools: network flooded with repeated /api/data calls.
  4. 13:06Console showed 'Maximum update depth exceeded'.
  5. 13:08Found useEffect dependency array included a selector returning a new array each render.
  6. 13:12Wrapped selector in useMemo, re-deployed, CPU normalized.

Right after pushing a new dashboard, alerts hit: user machines were freezing up and browser resource usage was off the charts. I opened DevTools and saw hundreds of fetches to /api/data every second.

Our useEffect was supposed to fetch data on filter changes, but a selector in the dependency array returned a new array on every render—triggering a re-fetch, which itself caused new renders, and so on.

Memoizing the selector output ended the loop immediately. It was a classic infinite loop, obscure because it hid inside a custom hook, not inline code.

Root cause

A selector returned a new array on every render, causing useEffect to re-run due to referential inequality.

The fix

Memoized the selector output with useMemo, making the dependency stable.

The lesson

Always check the referential stability of any value you include in a useEffect dependency array—especially output from custom hooks or selectors.

( 08 )Why useEffect Dependency Arrays Are (and Aren’t) Like Watchers

Unlike Angular or old React class lifecycles, useEffect’s dependency array uses referential equality. That means ({ foo: 1 }) !== ({ foo: 1 })—so including objects or arrays as dependencies triggers extra runs.

If you don’t understand how closures and value equality work, you’ll trip into these loops even with code that ‘looks right’. Always ask – is this dependency stable between renders? If not, memoize it.

( 09 )Hidden Traps With Custom Hooks and Selectors

Custom hooks, especially those returning derived data, are frequent sources of accidental infinite loops. For example, a selector that maps over Redux state and returns a new array each time.

Unless you use useMemo (or equivalent), that dependency will always be ‘new’ each render. The more layers of abstraction, the more likely you’ll miss these traps without defensive logging.

( 10 )Debugging Under Pressure: Fast Triage Steps

When production is melting, don’t guess. Immediately comment out the whole useEffect, reload, and see if the storm abates. If it does, binary search your dependencies by adding them back one at a time.

Log every dependency and its value or reference (use console.log(dep, typeof dep, dep === prevDep)). This catches subtly changing references.

Frequently asked questions

Can I just use an empty dependency array to stop the loop?

That runs the effect only once, but you’ll miss out on updates. Use an empty array only when you explicitly want ‘run on mount and never again’. Don’t use it to mask unstable dependencies.

Why does including a function in dependencies cause a loop?

If you define a callback inline or inside your component, it’s a new function each render, so useEffect sees it as changed and re-runs. Memoize functions with useCallback to keep references stable.

Should I disable the eslint-plugin-react-hooks rule if my effect is looping?

No—fix your closure or dependency stability instead. Disabling the rule hides real bugs and can lead to stale state problems downstream.

How do I handle unstable values from external libraries?

If you can’t control the library, memoize the value before passing it to your useEffect dependency array, or adapt your code to derive minimal, primitive dependencies.