The Stale Closure Problem in React.
How to Fix It ?
You can solve this problem in two ways, depending on how you want to manage your component.
1. Use a Functional State Updater
The cleanest fix is to stop reading the seconds variable from the outer scope entirely. Instead, pass a function into setSeconds.
useEffect(() => {
const interval = setInterval(() => {
// React automatically passes the freshest, current state into this callback
setSeconds((prevSeconds) => prevSeconds - 1);
}, 1000);return () => clearInterval(interval);
}, []); // Perfectly safe to leave empty now
By using prevSeconds => prevSeconds - 1, the interval no longer needs to remember what the time was when it started. It just tells React: “Whatever the current time in seconds is right now, take 1 away from it.” The closure is no longer stale because it doesn’t rely on the old snapshot variable anymore.
2. Put the “Variable” in the Dependency Array
If you prefer to use the standard seconds - 1 syntax, you have list the variable.
useEffect(() => {
const interval = setInterval(() => {
setSeconds(seconds - 1);
}, 1000);// Clear the old interval before the next render starts a new one
return () => clearInterval(interval);
}, [seconds]); // the variable here as it keeps changing
Now, every time seconds updates, React runs the cleanup function, removes the old interval, and runs the effect again. The new interval captures a fresh snapshot of the updated number. While this works perfectly, keep in mind that React is tearing down and rebuilding a timer every single second, which can affect the performance of your application.
The Lesson Learned
Stale closures are frustrating because the code looks perfectly valid. There are no syntax errors, and the logic makes sense to you.
When state is stuck in the past, check if your asynchronous functions or intervals are trapped inside an old render snapshot, switch to a functional state updater, (or add the dependencies, whichever one you want) and keep coding forward.