Back to articles
ReactHooks

The useEffect Mistakes I Made So You Don't Have To

An honest tour of the useEffect footguns I walked straight into — missing dependencies, effects that should've been event handlers, deriving state in effects, and forgetting to clean up.

Yash Thakur
4 min read
The useEffect Mistakes I Made So You Don't Have To

The short answer: The useEffect mistakes that cost me the most were lying to the dependency array to silence the linter, reaching for an effect when I really wanted an event handler, deriving state inside an effect instead of computing it during render, and forgetting to clean up subscriptions and timers. The through-line: most of the time the fix is not a better effect but no effect at all.

When hooks landed I was thrilled. No more this, no more lifecycle methods scattered across a class, no more binding handlers in the constructor. useEffect felt like the one tool that could do everything componentDidMount, componentDidUpdate, and componentWillUnmount used to do, all in one tidy function. That was exactly the problem. I treated it as a dumping ground, and it quietly punished me for years. Here are the mistakes, roughly in the order I made them.

Lying to the dependency array

My first instinct, when the linter screamed about a missing dependency, was to shut it up. I'd pass [] and move on, because adding the dependency "caused an infinite loop." It didn't cause an infinite loop — my effect was recreating the thing it depended on, and I was blaming the symptom.

The fix is almost always to be honest: list every value the effect reads, and if that causes churn, fix the churn (memoize the function, move it out, or question whether you need the effect at all). The dependency array isn't a config knob — it's a declaration of what the effect is a function of. Lie to it and you get stale closures that are miserable to debug.

Effects that were actually event handlers

This one took me the longest to unlearn. I'd fire analytics, show a toast, or POST something in an effect that watched a piece of state — because "when this changes, do that" felt like exactly what effects are for.

The rule I use now: if the code is reacting to a user doing something, it belongs in the handler. Effects are for synchronizing with something external — the DOM, a subscription, a timer, the network. "The user clicked save" is not external. Moving this logic into handlers deleted whole classes of double-fire bugs I'd been chasing.

Deriving state in an effect

I used to keep a second piece of state in sync with a first using an effect: fullName recomputed whenever firstName or lastName changed. It worked, but it meant an extra render every time and a window where the two were out of sync.

If you can calculate it from existing props or state, calculate it during render. No effect, no extra render, no stale window. State is for things React can't recompute for you; everything else is just a variable.

Forgetting to clean up

Subscriptions, timers, and in-flight requests all outlive the component if you let them. I shipped a search box that set state after unmount constantly, spraying warnings and occasionally rendering a result from a query the user had already navigated away from.

Every effect that starts something should return a function that stops it. Treat the missing return as a code smell, not an optional nicety.

The lesson underneath all of them

Every mistake above came from the same root: reaching for useEffect as a general-purpose "run some code" slot. It isn't. It's a synchronization tool with a specific job, and most of the time the honest answer is that you don't need one. Ask "what external system am I keeping in sync with?" — and if there isn't one, the effect probably shouldn't exist. I'd have saved myself a year of flaky bugs if someone had told me that on day one.

Written by Yash Thakur

Senior React Developer · 8+ years building for the web

More articles

Keep reading