React Hooks: Why useEffect is plotting against you
A survival guide for the dependency array minefield. Because infinite loops are only fun on rollercoasters.
Let’s talk about useEffect.
You know, that friendly little hook that React introduced to “simplify” lifecycle methods? The one that was supposed to save us from componentDidMount, componentDidUpdate, and componentWillUnmount hell?
Yeah, that one.
If you’ve ever stared at your browser console as it prints “Hello World” 5,000 times a second, you know exactly what I’m talking about.
The Infinite Loop of Doom
We’ve all been there. You just want to fetch some data when a prop changes. Simple, right?
useEffect(() => {
fetchData(userId); // wait, where did fetchData come from?
}, [userId]); // I'm safe... right?
But then your linter screams at you: “React Hook useEffect has a missing dependency: ‘fetchData’. Either include it or remove the dependency array.”
So, being the obedient developer you are, you add fetchData to the array.
useEffect(() => {
fetchData(userId);
}, [userId, fetchData]);
BOOM. Infinite loop. Your laptop fan spins up to takeoff speed. Your browser freezes. You question your life choices.
Why? Because fetchData is defined inside your component, meaning it’s a new function on every render. useEffect sees a “new” function, runs the effect, triggers a re-render, creating a “new” function… and welcome to the circle of life.
The useCallback Band-Aid
The solution, we are told, is useCallback.
const fetchData = useCallback(() => {
// actually fetch things
}, [userId]);
Great. Now we’re wrapping functions in hooks so we can use them in other hooks. It’s hooks all the way down.
Stale Closures: The Silent Killer
The other classic useEffect trap is the stale closure. It’s like a ghost from the past haunting your component.
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // why is count always 0?!
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array means "run once", right?
You think you’re clever with that []. But inside that interval, count will forever be the value it was when the component mounted. It doesn’t know about the new count. It’s living in the past, listening to My Chemical Romance and wearing skinny jeans.
So, what’s the fix?
- Stop lying to React. If you use a variable, put it in the dependency array. The linter connects you to reality.
- Use functional updates.
setCount(c => c + 1)saves you from needingcountin the dependency array. - Read the docs. seriously, the new React docs are actually good.
useEffect is powerful, but it’s like owning a tiger. It’s cool, but if you turn your back on it for one second, it will eat your face.
Happy coding, and may your dependency arrays always be stable!