Avoiding Infinite Loops in React’s useEffect
One of the most common React pitfalls is creating infinite loops with useEffect
due to incorrect dependency management. These loops can crash your application by causing unlimited re-renders.
Why Infinite Loops Happen
An infinite loop occurs when:
- Your
useEffect
updates state - That state is in its dependency array
- The update triggers the effect again
const [count, setCount] = useState(0);
// ❌ Infinite loop
useEffect(() => {
setCount(count + 1); // Updates count
}, [count]); // count is a dependency
Common Scenarios and Fixes
1. State Updates in useEffect
Problem:
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData); // Updates data
}, [data]); // ❌ data is dependency
Solution:
useEffect(() => {
fetchData().then(setData);
}, []); // ✅ Empty array if you only want to run once
2. Object/Array Dependencies
Problem:
const [user, setUser] = useState({ id: 1 });
useEffect(() => {
updateUser(user);
}, [user]); // ❌ New object reference every render
Solution:
const { id } = user;
useEffect(() => {
updateUser(user);
}, [id]); // ✅ Only depend on primitive value
3. Function Dependencies
Problem:
const fetchData = () => { /*...*/ };
useEffect(() => {
fetchData();
}, [fetchData]); // ❌ New function reference every render
Solution:
const fetchData = useCallback(() => {
/*...*/
}, []); // ✅ Stable function reference
useEffect(() => {
fetchData();
}, [fetchData]);
Advanced Patterns
1. Conditional State Updates
useEffect(() => {
if (needsUpdate) {
setData(computeNewData());
}
}, [needsUpdate]); // Only run when needsUpdate changes
2. Using Previous State
useEffect(() => {
setCount(prevCount => prevCount + 1); // Doesn't need count in deps
}, [trigger]); // Only runs when trigger changes
3. Deep Object Comparison
const [config, setConfig] = useState({ theme: 'dark' });
useEffect(() => {
console.log('Config changed');
}, [JSON.stringify(config)]); // Stringify for deep comparison
Debugging Infinite Loops
- Check your dependency array – Are you including state that gets updated?
- Look for state updates – Is any state being modified in the effect?
- Add console logs – Track effect executions and state changes
- Use React DevTools – Inspect component re-renders
Real-World Example
Problematic API Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ❌ Infinite loop - user changes every fetch
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId, user]);
return <div>{user?.name}</div>;
}
Corrected Version
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✅ Only runs when userId changes
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
Key Takeaways
- Never include in dependencies:
- State that gets updated in the effect
- Objects/arrays that change reference each render
- Use primitive values in dependency arrays when possible
- Stabilize references with
useCallback
anduseMemo
- Consider if you really need dependencies that cause re-runs
- Remember the ESLint plugin will warn about missing dependencies
Proper dependency management prevents infinite loops while ensuring your effects run when needed. Always question whether each dependency is truly necessary for your effect’s logic.