In 2026, why does setInterval in React always read the initial state?
The short answer: In 2026, when you debug a React timer problem, if the state read inside the setInterval callback is always the initial value, the usual cause is that the closure captured the state snapshot from the render that created the timer, not that the timer never fired. For “read latest value and then decide”, prefer a useRef mirror; for “accumulate from old value”, prefer setCount(prev => ...); only change the useEffect dependency when the timer really needs to be rebuilt according to a state.
First distinguish: is the callback “reading state” or “writing state”?
Mixing both actions is the easiest trap. For example, a callback first checks whether count is greater than 0, then calls setCount(count - 1). Here count comes from the first render. The UI often appears to advance only once and then stop, because every later calculation still uses the old copy.
How to judge: if state participates in an if condition, function argument, or display concatenation, it is a read scenario; if the callback only uses setXxx(prev => ...) to push the next value, it is a write scenario. Try not to put both kinds of logic in one callback.
useRef can read the latest value, but it is not a replacement for state
useRef returns the same object during the whole lifetime; accessing ref.current inside a callback always gives the most recently written value. State creates a new copy on every render, so old closures can only see old copies.
- Sync pattern: often pair it with a useEffect that writes the latest value into the ref.
- Rendering difference: changing ref.current does not trigger a render; the final display still needs setState.
- Responsibility boundary: do not put the entire page state into a ref.
How to choose among the three approaches: functional update, ref mirror, or restarting the timer
The three approaches serve different purposes; choose them by your data objective.
- Functional update: setCount(prev => prev + 1) does not read the outer closure and is suitable for countdowns and accumulators. The experience range for a single change is roughly ten minutes to one hour; the limitation is that you cannot read other state inside the callback for complex judgment.
- Ref mirror: suitable for reading the latest value such as a user preference or server offset and then making decisions. It requires an additional sync effect; introducing it in one place has an experience range of about half an hour to one hour, while syncing multiple places takes about half a day.
- Restart the timer: put the dependent value in the useEffect dependency array and clean up plus rebuild the interval when it changes. This is suitable for timing conditions that change with low-frequency state. If state changes every second, the interval will restart repeatedly and the delay will always start over.
Field implementation: a time-sync countdown stuck after an old snapshot
Earlier, a delivered countdown for “10 minutes before meeting start” required checking time with the server every minute. The constraint was that the visible seconds could not jump backwards and the page could not visibly stutter. The first version placed the server time offset into the useEffect dependency, and the interval was destroyed and rebuilt every minute; in practice, the seconds jumped.
Later, the time offset was moved into a ref; setInterval was only responsible for calculating “local time + offset” every second and calling setState; the useEffect dependency remained an empty array and returned clearInterval. The seconds no longer jumped backward in production. The cost is the extra ref synchronization logic, and maintainers must understand closures before touching it. The experience range for a similar fix is about half an hour to one hour for a single component; checking and regression-testing across multiple pages takes about half a day to one day.
Three things to verify: no duplicate timers under StrictMode; after manual operations the next tick no longer reads old values; switching the page to the background and back does not stack extra timers.
Applicable use cases and non-applicable boundaries
This set of solutions solves the stale state snapshot caused by closures; it does not solve problems where the timer itself is the wrong tool.
- Applies to: low-frequency polling, countdowns, carousels, periodic local-state synchronization, and cases not sensitive to a single delayed tick.
- Not applicable: high-frequency animation; requestAnimationFrame should be the first choice, do not use setInterval to simulate a frame rate.
- No need to use: one-time delays should use setTimeout; API polling can use recursive setTimeout and remember to pause when the tab is in the background.
- Time-source reminder: displaying server time should use the server reference; the local timer is only responsible for refreshing the render when it expires.
FAQ
Why is count inside setInterval always the initial value?
When the setInterval callback is created, the closure captures the count from that render. Later count updates do not change the closure, so every execution still sees the old copy.
If a useRef mirror can get the latest value, can that value be rendered directly on the page?
No. Changing ref.current does not trigger component re-render; setState is still required to update the page. The ref is only a channel for the callback to read the latest value.
Can functional updates and a ref mirror be used together?
Yes. Use the ref for reads/decisions and functional updates for state advancement. Avoiding reading and writing state in the same callback reduces side effects caused by stale snapshots.
Can turning off StrictMode avoid duplicate timers?
It is not recommended to solve it by disabling strict mode. Double mounting in development can be treated as a reminder; you should clearInterval in the useEffect cleanup function and check the dependency array.
I wrote the timer cleanup function, but count is still old — what am I missing?
The cleanup function only removes the old interval; it does not replace the count in the closure. If state is still read directly, the new interval will capture the current state snapshot again when it is created.
In React projects in 2026, treat timer callbacks as long-lived code that is rarely rebuilt: use refs to read the latest state, hand cumulative changes to functional updates, and accept that the interval will restart when you need to restart the timer. Handling it in this order usually converges within one round of changes; if it remains unresolved, then check the render frequency, cleanup timing, and server time source.
-
It’s 2026. Everyone nodded in requirements review—why are empty states and loading states still being filled in during integration?
Date: Sep 14, 2026 Read: 2
-
CDN refreshed, why are some users still seeing the old page?
Date: Sep 13, 2026 Read: 6
-
When Uni-app ships both a mini program and an app and platform differences keep piling up, where should you start in 2026?
Date: Sep 12, 2026 Read: 12
-
In 2026, is a large Flutter install package the engine's fault or too much bundled in the project?
Date: Sep 11, 2026 Read: 13
-
Does a page URL with a # affect indexing in front-end SEO (Google/Baidu)?
Date: Sep 10, 2026 Read: 21




