Build1 distinct publisher3 min readUpdated
A dev.to walkthrough restates the oldest React timer bug: setInterval closes over one render. The durable fix is a ref-held callback keyed only on delay, an idea Dan Abramov published in 2019.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A walkthrough published on dev.to reopens the most familiar failure in React code: a counter wired up with `useEffect`, an empty dependency array and `setInterval(() => setCount(count + 1), 1000)` renders 0, then 1, and then sits at 1 forever [1]. It matters because the two fixes that surface first either wreck the timing or narrow what the callback is allowed to read [3][4].
The mechanism is not subtle. The interval callback was created during the first render, when `count` was 0, and an empty dependency array guarantees it never sees another render, so `setCount(0 + 1)` fires every second and changes nothing [2]. At a one second delay that is roughly 3,600 state updates an hour, every one of them setting `count` to 1 [1]. The post's author calls it the single most searched React timer bug [cA].
Fix one is to add `count` to the effect's dependencies. The reads become correct and the timer becomes garbage: it is torn down and re-created every second, the timing resets on each change, and with a fast-changing dependency the tick may never fire at all [3]. Fix two, the functional updater, holds up until the callback needs anything other than the previous count [4]. Both are attempts to negotiate with a mismatch that does not negotiate. `setInterval` is imperative and lives outside React's render cycle, while everything it wants to read lives inside it [5].
Dan Abramov's 2019 essay "Making setInterval Declarative with React Hooks" is where the actual answer comes from: keep the latest callback in a ref, and never restart the timer just because the component re-rendered [6]. The hand-rolled version is about a dozen lines. A `savedCallback` ref, a `useLayoutEffect` keyed on the callback that assigns the freshest closure into it, and a `useEffect` keyed only on `delay` that bails out when `delay` is `null` and otherwise creates `setInterval(() => savedCallback.current(), delay)` with a `clearInterval` cleanup [7].
Two consequences fall out of that shape, and they are the reason to prefer it. Changing the callback never restarts the timer, so inline arrow functions and direct state reads are both fine and the interval keeps its rhythm [9]. Changing `delay` does restart it, because `delay` is the only dependency: 5000 to 1000 clears the old timer and starts a new one, which resets the phase so the next tick lands a full delay after the change committed [10]. That is correct for backoff and mildly surprising if you expected the in-flight tick to finish [10].
The write-up frames three distinct problems the pattern absorbs: stale closures, restart-on-render, and lifecycle [14]. `setInterval` holds one function reference for its whole life, closed over one render's props and state, and every later render mints a closure the running timer never sees [15]. Lifecycle is the one that quietly grows: clear on unmount, clear again on StrictMode's development remount, and pausing turns into a small state machine of extra state, an `if` around `setInterval`, and dependencies that now include the pause flag [16].
`useInterval` from `@reactuses/core` packages the same core, using `useLatest` for the ref, and adds the parts a real app needs [8][13]. Signature is `useInterval(callback, delay, options?)`, delay in milliseconds or `null` to pause, returning `{ isActive, pause, resume }` [11]. Swap it in and the callback reads props and state directly, the timer is created once and cleared on unmount, and there is no dependency array to get wrong [12].
What to watch: the `delay` phase reset, since any dynamic polling interval will silently reschedule from the commit moment [10]. The same post goes on to cover two pause strategies, dynamic intervals, the background-tab problem and when a different timer hook is the better choice [17], and background throttling is the constraint no ref pattern removes.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The example broken component uses useState(0) and a useEffect with an empty dependency array containing setInterval(() => setCount(count + 1), 1000) with a clearInterval cleanup; it renders 0, then 1, and stays at 1 forever.
The interval callback was created on the first render, where count was 0, and an empty dependency array means it never sees another render, so setCount(0 + 1) runs every second and nothing changes.
Adding count to the effect's dependency array makes the interval read correctly but tears it down and re-creates it every second; the timing resets each time, and with a fast-changing dependency the tick may never fire at all.
Using the functional updater works until the callback needs anything other than the previous count.
setInterval is imperative and lives outside React's render cycle, while everything it wants to read lives inside it.
Dan Abramov's 2019 essay "Making setInterval Declarative with React Hooks" proposed a useInterval hook that keeps the latest callback in a ref and never restarts the timer just because the component re-rendered.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Self-demonstrating code, one publisher
The technical core is verifiable from the source itself: the failing component, the three failure modes, and the corrected implementation are printed in full, and the pattern is attributed to a named 2019 essay. But the cluster has exactly one source, the claims about @reactuses/core's internals and StrictMode-safe cleanup rest solely on the author's word, the headline superlative is unsupported, and the body is truncated before the background-tab and hook-selection sections it promises.
No adoption signal supplied
The cluster contains no downloads, dependents, release notes, deployment reports, benchmarks, or usage disclosures for @reactuses/core or the pattern. An npm install line in a tutorial is an instruction, not evidence of uptake, so adoption cannot be scored.
Mildly overstated framing over sound technique
The engineering content is standard and well supported, so the gap is small. It is positive rather than zero because the piece attaches an unmeasured superlative ('the single most searched React timer bug') and a 2026 dateline to a solution published in 2019, and presents a specific package as the resolution while supplying no evidence of that package's adoption, maintenance, or actual internals.
Package-promotional tutorial, no disclosure
The article's structure — problem, folk fixes that fail, a hand-rolled version, then 'that's essentially the core of @reactuses/core' plus an install command — routes the reader to one named npm package, and the post carries no disclosure of the author's relationship to it. That is a visible promotional interest. It is not scored higher because the recommended technique is independently attributable to Abramov's 2019 essay and the post also publishes a dependency-free implementation readers can copy instead.
High on mechanics, low on everything else
Confidence in the code-level mechanics is high because they are printed and self-checking, but the cluster is one author-submitted post from one publisher, with no corroboration, no adoption data, a promotional interest, and a truncated body. The composite therefore sits below the midpoint.
build
The Escape-key listener you cannot fix with a dependency array1 distinct publisher
build
The stale closure is not React trivia, it is a correctness bug with a five-line fix1 distinct publisher
build
Your half-finished checkout form does not belong in localStorage1 distinct publisher
build
Three attackers hide behind one connect button, and encryption only stops one of them1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026