The Problem: useEffect Runs Forever

Your React component renders, the effect runs, the effect updates state, the component re-renders, and the cycle repeats until the browser freezes. The console fills with warnings or your app simply becomes unresponsive.

This is one of the most common React bugs, and it almost always traces back to a non-primitive value in the useEffect dependency array.

Symptoms

  • The browser becomes unresponsive or extremely laggy
  • Console shows Maximum update depth exceeded warning
  • Network requests fire repeatedly in DevTools Network tab
  • CPU usage spikes to 100% for a single tab
  • React DevTools shows the component rendering hundreds of times per second

The Root Cause

Objects and arrays in JavaScript are compared by reference, not by value. Every time a component renders, a new object literal {} or array literal [] creates a brand-new reference.

```javascript // This creates an INFINITE LOOP function UserProfile({ userId }) { const [user, setUser] = useState(null);

useEffect(() => { fetchUser(userId).then(setUser); }, [userId, { includeProfile: true }]); // <-- New object every render!

return <div>{user?.name}</div>; } ```

Even though { includeProfile: true } looks identical each time, it is a different object in memory. React sees a changed dependency, re-runs the effect, which triggers a re-render, which creates another new object, and the loop continues.

Real Error Message

bash
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.
    at UserProfile
    at App

How to Fix It

Fix 1: Use useMemo for Stable References

```javascript function UserProfile({ userId }) { const [user, setUser] = useState(null); const config = useMemo(() => ({ includeProfile: true }), []);

useEffect(() => { fetchUser(userId, config).then(setUser); }, [userId, config]);

return <div>{user?.name}</div>; } ```

useMemo memoizes the object so the same reference is returned across renders.

Fix 2: Extract Primitive Values

```javascript function UserProfile({ userId }) { const [user, setUser] = useState(null);

useEffect(() => { fetchUser(userId, { includeProfile: true }).then(setUser); }, [userId]); // Only depend on primitives

return <div>{user?.name}</div>; } ```

If the configuration never changes, just inline it inside the effect and only list primitive dependencies.

Fix 3: Use useRef for Mutable Configuration

```javascript function UserProfile({ userId }) { const [user, setUser] = useState(null); const configRef = useRef({ includeProfile: true });

useEffect(() => { fetchUser(userId, configRef.current).then(setUser); }, [userId]);

return <div>{user?.name}</div>; } ```

useRef gives you a stable reference that persists across renders without triggering re-renders when updated.

Prevention

  • Never put object literals or array literals directly in dependency arrays
  • Use the react-hooks/exhaustive-deps ESLint rule to catch missing dependencies
  • Use React DevTools Profiler to spot components re-rendering excessively
  • Prefer primitive values (string, number, boolean) in dependency arrays
  • When you need complex config, memoize it with useMemo or store it with useRef