Fix React useEffect Infinite Loops: Root Causes and Minimal Checks

Topic: react-useeffect-infinite-loop-fixUpdated 7/23/2026

Quick Answer

  • Root cause: Infinite loops occur when a state update inside useEffect triggers a re-render, which re-runs the effect because a dependency changed, creating an endless cycle.
  • First checks: Verify your dependency array includes every variable used inside the effect, and ensure no object/array literals or inline functions are passed as dependencies (they change on every render).
  • Minimal fix: Add missing dependencies to the array, or use useMemo/useCallback to stabilize object/function references before passing them as dependencies.
  • Applicable environment: React 16.8+ with Hooks; be aware that React 18 Strict Mode intentionally double-invokes effects in development to surface bugs.

What Problem It Solves

React's useEffect Hook lets you perform side effects in function components. When the dependency array is missing, incorrect, or contains unstable references, the effect runs on every render instead of only when intended. This causes:

  • Infinite re-render loops that crash the browser with "Maximum update depth exceeded"
  • Unnecessary API calls and performance degradation
  • Stale closures that read outdated state or props

This guide provides a practical, 5-minute checklist to identify and fix the three most common infinite-loop patterns.

Root Cause Analysis

The useEffect Hook runs after every render where any dependency in its array has changed. An infinite loop happens when:

  1. The effect updates state
  2. The state update triggers a re-render
  3. The re-render produces a new value for a dependency
  4. The effect runs again because the dependency changed
  5. Go to step 1

Three patterns cause unstable dependencies:

PatternExampleWhy It Loops
Missing dependencyuseEffect(() => setCount(count + 1), [])Effect reads count but doesn't list it; React warns but the value is stale. If you add it, the effect runs every time count changes, creating a loop.
Object/array literaluseEffect(fn, [{ key: value }])A new object is created on every render, so the dependency is always "changed."
Inline functionuseEffect(fn, [() => {}])A new function reference is created on every render.

Minimal Working Configuration

Pattern 1: State Update Without Conditional Guard

JSX
// ❌ Infinite loop: setState runs unconditionally
useEffect(() => {
  setCount(count + 1);
}, [count]);

// ✅ Fix: add a condition to stop the loop
useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

Pattern 2: Object or Array as Dependency

JSX
// ❌ Infinite loop: new object every render
const config = { theme: 'dark' };
useEffect(() => {
  fetchData(config);
}, [config]);

// ✅ Fix: memoize the object
const config = useMemo(() => ({ theme: 'dark' }), []);
useEffect(() => {
  fetchData(config);
}, [config]);

Pattern 3: Function as Dependency

JSX
// ❌ Infinite loop: new function every render
const handleClick = () => setOpen(!open);
useEffect(() => {
  window.addEventListener('click', handleClick);
  return () => window.removeEventListener('click', handleClick);
}, [handleClick]);

// ✅ Fix: memoize the function
const handleClick = useCallback(() => setOpen(prev => !prev), []);
useEffect(() => {
  window.addEventListener('click', handleClick);
  return () => window.removeEventListener('click', handleClick);
}, [handleClick]);

Common Errors and Fixes

Error: "React Hook useEffect has a missing dependency: 'xxx'"

Solution: Add the missing dependency to the array. If you intentionally omit it, use // eslint-disable-next-line react-hooks/exhaustive-deps with a comment explaining why.

JSX
useEffect(() => {
  // This effect should only run once on mount
  fetchInitialData();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Error: "Maximum update depth exceeded"

Solution: Check if setState is called unconditionally inside the effect. Add a conditional guard or use useRef to track whether the effect has already run.

JSX
const hasRun = useRef(false);
useEffect(() => {
  if (!hasRun.current) {
    hasRun.current = true;
    setData(fetchData());
  }
}, []);

Error: "React Hook useEffect has a spread element in its dependency array"

Solution: List each property individually instead of spreading an object.

JSX
// ❌ Not supported
useEffect(fn, [...obj]);

// ✅ Correct
useEffect(fn, [obj.a, obj.b]);

Error: "Can't perform a React state update on an unmounted component"

Solution: Use a cleanup function with a cancellation flag or AbortController.

JSX
useEffect(() => {
  let cancelled = false;
  const controller = new AbortController();

  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setData(data);
    })
    .catch(err => {
      if (err.name !== 'AbortError' && !cancelled) setError(err);
    });

  return () => {
    cancelled = true;
    controller.abort();
  };
}, []);

Production Notes and Security Checks

  • React 18 Strict Mode: In development, effects run twice to detect bugs. If your code breaks under double invocation, fix the cleanup logic—don't disable Strict Mode.
  • Memory leaks: Always return a cleanup function for subscriptions, timers, and async operations.
  • Security: Never pass unsanitized user input directly as a dependency without validation. An attacker could craft input that causes excessive re-renders or exposes internal state.
  • Performance: Avoid large objects or arrays as dependencies. Memoize them with useMemo or restructure to use primitive values.
  • Server-side rendering (SSR): useEffect does not run during SSR. If your component relies on effects for initial data, use a data-fetching library like React Query or SWR that handles SSR hydration.

FAQ

Q: In React 18 Strict Mode, why does useEffect run twice? Is this a bug?

A: This is intentional. Strict Mode double-invokes effects (including cleanup) in development to help you find bugs: missing cleanup functions, non-reentrant side effects, and memory leaks. Production builds do not double-invoke. If your code breaks, fix the effect logic rather than disabling Strict Mode.

Q: What's the difference between useEffect and useLayoutEffect? When should I use useLayoutEffect?

A: useEffect runs asynchronously after the browser paints; useLayoutEffect runs synchronously after DOM mutations but before the browser paints. Use useLayoutEffect when you need to read layout (e.g., element dimensions) and synchronously trigger a re-render to avoid visual flicker. For most cases, useEffect is sufficient. Note: useLayoutEffect warns in SSR environments.

Q: How do I correctly use async/await inside useEffect?

A: Do not make the effect callback itself async—it returns a Promise, but useEffect expects either undefined or a cleanup function. Define and call the async function inside the effect:

JSX
useEffect(() => {
  const fetchData = async () => {
    try {
      const result = await api.getData();
      setData(result);
    } catch (error) {
      setError(error);
    }
  };
  fetchData();
  return () => { /* cleanup */ };
}, [deps]);

Always include cancellation logic (via AbortController or a boolean flag) to prevent state updates on unmounted components.

Related Guides