Hey there. If you’ve been working with React for a while, you’ve probably banged your head against the wall because of useEffect. It’s powerful, but it’s also the easiest place to introduce bugs like infinite loops or stale data.
I remember when I first started using hooks, I treated useEffect like componentDidUpdate, and boy, was that a mistake. It felt like I was fighting the framework instead of working with it.
Today, I want to walk you through some common mistakes I’ve made (and seen others make) and how to fix them properly.
1. Missing Dependency Array
This is the classic infinite loop generator. If you omit the dependency array, the effect runs after every render. If your effect updates state, that triggers a re-render, which triggers the effect again… and you get the picture.
Here is what it looks like when things go wrong:
// ❌ BAD: Runs on every render
useEffect(() => {
setCount(count + 1);
});
To fix this, you need to tell React when to run the effect. If you only want it to run once on mount, pass an empty array [].
// ✅ GOOD: Runs only on mount
useEffect(() => {
console.log("Component mounted");
}, []);
2. Lying to React about Dependencies
Sometimes ESLint yells at you because you used a variable inside useEffect but didn’t put it in the dependency array. So, you just suppress the warning, right?
Please don’t. That leads to “stale closures,” where your effect uses old values of variables because it hasn’t re-run to capture the new ones.
// ❌ BAD: 'count' will always be the initial value inside the interval
useEffect(() => {
const id = setInterval(() => {
console.log(count); // stale value!
}, 1000);
return () => clearInterval(id);
}, []); // Lying that we don't depend on 'count'
The fix is to be honest. If you use it, list it.
// ✅ GOOD: Effect re-runs when count changes
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);
Or better yet, use the functional update form if you’re setting state, so you don’t need count as a dependency at all.
3. Not Cleaning Up
useEffect is also where we handle side effects like subscriptions or event listeners. If you don’t clean them up, they pile up every time the component re-renders or unmounts, causing memory leaks.
// ❌ BAD: Adds a new listener on every render, never removes them
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
});
Always return a cleanup function.
// ✅ GOOD: cleans up the previous listener before adding a new one
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
Conclusion
useEffect isn’t magic. It’s just a way to synchronize your component with some external system. Respect the dependency array, clean up your messes, and you’ll be fine.
I hope this saves you some debugging time. Happy coding!