Skip to content

How to Fix useEffect Infinite Render Loops in React

Problem

I was building a React component that needed to fetch data on mount and update some local state. Everything seemed fine until I opened my browser console and saw this:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either depends on the state or has no dependency array at all.

My app became unresponsive, the CPU fan started spinning loudly, and I realized I had created an infinite render loop. Here’s what my problematic code looked like:

UserProfile.jsx
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true); // Setting state inside useEffect
fetchUserData().then(data => {
setUser(data);
setLoading(false);
});
}, [user]); // Problem: 'user' is in the dependency array
return <div>{user?.name}</div>;
}

I thought I was being careful by specifying dependencies, but I had just built a “fragile Rube Goldberg machine of dependency arrays” as one Reddit commenter perfectly described it.

Root Cause Analysis

The infinite loop happens because of a self-perpetuating cycle:

┌─────────────────────────────────────────────────────────┐
│ │
│ Component Renders │
│ │ │
│ ▼ │
│ useEffect Runs │
│ │ │
│ ▼ │
│ State Changes (setUser/setLoading) │
│ │ │
│ ▼ │
│ Component Re-renders │
│ │ │
│ ▼ │
│ useEffect Runs Again (because 'user' changed) │
│ │ │
│ ▼ │
│ ┌─────┴─────┐ │
│ │ LOOP │ ◄─────────────────────────────────────┘ │
│ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

The problem in my code was that:

  1. useEffect runs on initial render
  2. Inside the effect, I call setUser(data)
  3. The component re-renders because state changed
  4. useEffect runs again because user is in the dependency array
  5. The cycle repeats forever

This pattern is so common that the React team created a specific ESLint rule for it.

Solution 1: Remove Unnecessary Dependencies

The simplest fix was to remove user from the dependency array since I only wanted to fetch data once on mount:

UserProfile.jsx
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUserData().then(data => {
setUser(data);
setLoading(false);
});
}, []); // Empty array: run only on mount
return <div>{user?.name}</div>;
}

This works when you genuinely only need to run the effect once.

Solution 2: Use Functional Updates

When you need to update state based on previous state, use functional updates instead of reading the state directly:

Counter.jsx
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// Using functional update - no need for 'count' in deps
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // 'count' not needed here
return <div>Count: {count}</div>;
}

The functional update form setCount(prevCount => prevCount + 1) doesn’t require count in the dependency array because it receives the current state as an argument.

Solution 3: Restructure Derived State

Sometimes the infinite loop happens because you’re creating derived state. Instead of syncing state in a useEffect, compute it during render:

UserProfile.jsx
// BAD: Syncing derived state with useEffect
function UserProfileBad({ userId }) {
const [user, setUser] = useState(null);
const [displayName, setDisplayName] = useState('');
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
});
}, [userId]);
useEffect(() => {
if (user) {
setDisplayName(user.name.toUpperCase()); // Infinite loop risk
}
}, [user]); // Sets state when user changes
return <div>{displayName}</div>;
}
// GOOD: Compute derived state during render
function UserProfileGood({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
});
}, [userId]);
// Compute during render - no useEffect needed
const displayName = user?.name.toUpperCase() ?? '';
return <div>{displayName}</div>;
}

As one developer noted: “useEffect → derived state → another useEffect. Built that Rube Goldberg machine too many times.”

Solution 4: Use useRef for Mutable Values

When you need a mutable value that doesn’t trigger re-renders, use useRef:

Timer.jsx
function Timer() {
const [seconds, setSeconds] = useState(0);
const prevSecondsRef = useRef(0);
useEffect(() => {
prevSecondsRef.current = seconds;
}, [seconds]);
// Can use prevSecondsRef.current without causing loops
const secondsChanged = prevSecondsRef.current !== seconds;
return (
<div>
Seconds: {seconds}
{secondsChanged && <span> (changed!)</span>}
</div>
);
}

Enable the ESLint Rule

The React team provides an ESLint rule that catches this pattern. Enable it in your ESLint config:

.eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/set-state-in-effect': 'error',
},
};

This rule specifically warns when you’re setting state inside a useEffect that has that same state in its dependency array.

Best Practices Checklist

Before committing code with useEffect, verify:

  • Every dependency in the array is actually needed
  • State updates inside the effect don’t trigger the effect again
  • Derived state is computed during render, not synced with effects
  • Functional updates are used when updating based on previous state
  • The ESLint react-hooks plugin is enabled

Summary

Infinite render loops in useEffect occur when state changes trigger the same effect repeatedly. The key fixes are:

  1. Empty dependency array for mount-only effects
  2. Functional updates for state based on previous state
  3. Derived state computed during render instead of synced with effects
  4. useRef for values that shouldn’t trigger re-renders

The React ESLint plugin’s set-state-in-effect rule catches this pattern automatically. Enable it and save yourself from building fragile Rube Goldberg machines.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments