Empower growth and innovation with the latest Front-end Dev insights

In 2026, why does setInterval in React always read the initial state?

Sep 7, 2026 Read: 26

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.

Have a similar project in mind?
Contact us for a one-to-one project reference proposal
Obtain Proposal
Interested in this topic?
10-year tech team — reference proposal within 24 hours
Obtain Proposal
Are you ready?
Then reach out to us!
+86-13370032918
Discover more services, feel free to contact us anytime.
Please fill in your requirements
What services would you like us to provide for you?
Your Budget
ct.
Our WeChat
Professional technical solutions
Phone
+86-13370032918 (Manager Jin)
The phone is busy or unavailable; feel free to add me on WeChat.
E-mail
349077570@qq.com
Submitted successfully
Thank you for your trust. We will contact you soon!
Recommended projects for you