This is premium content - thanks for subscribing! useEffect causes more subtle bugs for junior React developers than almost any other hook, because it looks simple but hides several sharp edges.

The stale closure trap

When your effect closes over a variable from an earlier render and the dependency array doesn't include it, you'll keep reading an outdated value indefinitely. Always let the linter's exhaustive-deps rule guide your dependency array instead of suppressing it.

Cleanup timing

React runs your cleanup function before every re-run of the effect, not just on unmount. For subscriptions, timers, and event listeners, forgetting this means you accumulate duplicate listeners every time a dependency changes.

Dependency array mistakes

Objects and arrays created inline in the render body are a new reference every render, so putting them in a dependency array causes the effect to run on every render - defeating the purpose of the array entirely. Memoize them with useMemo/useCallback, or restructure the effect to depend on primitive values instead.

Takeaway

Treat useEffect dependencies as a contract, not a suggestion - the bugs it prevents are exactly the ones that are hardest to spot in code review.

Diagram of the React useEffect render, effect, and cleanup cycle