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:
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:
useEffectruns on initial render- Inside the effect, I call
setUser(data) - The component re-renders because state changed
useEffectruns again becauseuseris in the dependency array- 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:
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:
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:
// BAD: Syncing derived state with useEffectfunction 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 renderfunction 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:
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:
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:
- Empty dependency array for mount-only effects
- Functional updates for state based on previous state
- Derived state computed during render instead of synced with effects
- 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