useEffect Cleanup: Why It Matters and Common Mistakes
Understanding React’s Essential Memory Management Tool
useEffect Cleanup: Why It Matters and Common Mistakes

Understanding React’s Essential Memory Management Tool
When working with React, useEffect is one of the most powerful yet misunderstood hooks. While many developers know how to use it to fetch data or subscribe to events, the cleanup function is often overlooked — leading to memory leaks, bugs, and unexpected behavior.
That small return function is what prevents memory leaks, unwanted background tasks, and state updates after unmount.
In this article, we’ll explore why cleanup matters and the most common mistakes developers make.
What is useEffect Cleanup?
The cleanup function is the return function inside useEffect. It runs:
- Before the component unmounts
- Before the effect runs again (on re-renders with changed dependencies)
useEffect(() => {
// Setup code (runs on mount/update)
return () => {
// Cleanup code (runs before unmount/next effect)
};
}, [dependencies]);
Think of it as React’s way of saying:
“Clean up your mess before leaving!”
Why Cleanup Matters
1. Preventing Memory Leaks
Without proper cleanup, you might keep references to components that no longer exist.
// ❌ Bad: Memory leak
useEffect(() => {
const interval = setInterval(() => {
console.log('Still running...');
}, 1000);
// No cleanup! Interval keeps running forever
}, []);
// ✅ Good: Proper cleanup
useEffect(() => {
const interval = setInterval(() => {
console.log('Running...');
}, 1000);
return () => {
clearInterval(interval); // Stops when component unmounts
};
}, []);
2. Avoiding State Updates on Unmounted Components
Ever seen this warning?
⚠️ “Can’t perform a React state update on an unmounted component”
// ❌ Bad: State update after unmount
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data); // Component might be unmounted!
});
}, []);
// ✅ Good: Using cleanup flag
useEffect(() => {
let isMounted = true;
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (isMounted) {
setData(data);
}
});
return () => {
isMounted = false;
};
}, []);
3. Using AbortController (Modern Approach)
// ✅ Better: Using AbortController
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
const data = await response.json();
setData(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(error);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, []);
Common Mistakes and How to Fix Them
Mistake #1: Forgetting to Clean Up Event Listeners
// ❌ Bad: Event listener never removed
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
// ✅ Good: Properly cleaned up
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
Mistake #2: Not Cleaning Up Subscriptions
// ❌ Bad: WebSocket stays open
useEffect(() => {
const socket = new WebSocket('ws://example.com');
socket.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
}, []);
// ✅ Good: WebSocket properly closed
useEffect(() => {
const socket = new WebSocket('ws://example.com');
socket.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => {
socket.close();
};
}, []);
Mistake #3: Cleaning Up the Wrong Reference
// ❌ Bad: Wrong function reference
useEffect(() => {
const handleScroll = () => {
console.log('Scrolling');
};
window.addEventListener('scroll', handleScroll);
return () => {
// This creates a NEW function - won't remove the original!
window.removeEventListener('scroll', () => {
console.log('Scrolling');
});
};
}, []);
// ✅ Good: Same function reference
useEffect(() => {
const handleScroll = () => {
console.log('Scrolling');
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
Mistake #4: Missing Dependencies Leading to Stale Closures
// ❌ Bad: Stale closure problem
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // Always logs initial value!
}, 1000);
return () => clearInterval(interval);
}, []); // Missing 'count' dependency
}
// ✅ Good: Updated dependency array
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // Logs current value
}, 1000);
return () => clearInterval(interval);
}, [count]); // Cleanup runs, new interval created
}
Mistake #5: Not Handling Async Cleanup
// ❌ Bad: Race condition with async operations
useEffect(() => {
const loadUser = async () => {
const user = await fetchUser(userId);
setUser(user); // Might set wrong user!
};
loadUser();
}, [userId]);
// ✅ Good: Handling race conditions
useEffect(() => {
let cancelled = false;
const loadUser = async () => {
const user = await fetchUser(userId);
if (!cancelled) {
setUser(user);
}
};
loadUser();
return () => {
cancelled = true;
};
}, [userId]);
Cleanup Needed
- setInterval
- setTimeout
- Event listeners
- WebSockets
- Fetch requests (AbortController)
- Subscriptions (RxJS, etc.)
Cleanup Not Needed
- Simple state updates
Key Takeaways
- Always clean up timers, event listeners, and subscriptions
- Use AbortController for fetch requests
- Handle race conditions in async effects
- Use the same reference when removing event listeners
- Include all dependencies to avoid stale closures
Related Articles:
Conclusion
The cleanup function in useEffect isn't optional — it's essential for building robust React applications. By understanding when and how to clean up, you'll avoid memory leaks, prevent bugs, and write more maintainable code.
Next time you write a useEffect, ask yourself: "What needs to be cleaned up?"
Thanks for reading!
If you found this useful, hit the share button and leave a comment below.
You can also follow me on **GitHub and [LinkedIn](https://www.linkedin.com/in/om-prakash-sah) **for more updates.
Happy coding! 🚀
메타데이터
- post_id
- 01a0107f98ee
- slug
- useeffect-cleanup-why-it-matters-and-common-mistakes-01a0107f98ee
- url
- https://medium.com/@kom50/useeffect-cleanup-why-it-matters-and-common-mistakes-01a0107f98ee
- canonical_url
- https://medium.com/@kom50/useeffect-cleanup-why-it-matters-and-common-mistakes-01a0107f98ee
- author_url
- https://medium.com/@kom50
- status
- ok
- fetched_at
- 2026-06-14 11:28:49