Hasan's Journal

Stories, lessons, and scars from production.

Mehedi Hasan
Back to blog

Debugging Stale Closures in React: A Friday Story

A useEffect with a correct-looking dependency array kept reading stale state. The culprit was a closure, not the code — and the lesson stuck.

#React#Debugging

The Bug

It was a Friday, 4:30 in the afternoon, and a useEffect hook with a dependency array that looked entirely correct kept reading state that was several renders out of date. The dependency array had the variable, the variable was being updated, the component was re-rendering — but the effect kept seeing the old value as if frozen in time. I went through the usual checklist — refactoring, forcing re-renders, restarting the dev server — and none of it changed the behavior. The bug was real, reproducible, and completely inexplicable from the code as written.

The culprit was a stale closure: I was calling a state setter with a callback that referenced an external variable captured from the render in which the effect was originally created, rather than the variable's current value at the moment the effect ran. JavaScript closures capture variables by reference, but in React, each render creates new versions of variables in scope, and an effect created in one render holds onto the versions from that render. If the effect doesn't list the variable in its dependency array, React doesn't know to recreate the effect when the variable changes. The dependency array isn't just a performance optimization — it's the mechanism React uses to know when an effect's closure has gone stale.

The fix was switching to the functional update form — `setState(prev => prev + 1)` instead of `setState(count + 1)` — which reads the latest state directly from React rather than from a closure that may have gone stale. Since that Friday, checking for stale closures is the first thing I look for whenever a useEffect appears to be reading outdated values, before considering more exotic explanations. That single habit has caught the same underlying bug at least three more times since.