Build1 distinct publisher3 min readUpdated
An autosave button that reports "saved" while dropping the user's last three words is the same defect as a stale socket handler. The fix is a ref written in a layout effect.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A dev.to walkthrough of a React autosave button ends with the UI reporting "saved" while the user's last three words are not saved, because the callback read text captured from the render where the click happened [1][2]. That deserves an operator's attention because the same failure mode is latent in every callback that outlives its render: setTimeout and setInterval, code after an await, listeners registered once, IntersectionObserver and ResizeObserver callbacks, WebSocket onmessage, and any third-party SDK that takes a callback at construction time [3].
Read the sequence as a timing defect rather than a framework quirk. save() sets status to "saving", awaits an 800 ms api.save call, then sets status to "saved"; the user types three more words while the request is in flight, and those words are neither sent nor flagged [1]. The function needs two different values: what it sent, and what the document contains when the promise resolves. A closure can only supply the first [4], and no amount of re-rendering rewrites it [2].
React's own FAQ answers the stale props and state question with a ref that always holds the latest value [5]. The package @reactuses/core ships that as useLatest: five lines, no re-renders, no dependency arrays [6]. It returns a MutableRefObject whose .current is the most recently rendered value, and the ref object's identity never changes, so it is safe to close over from a timer, a promise or a subscription [7]. The corrected example keeps both readings: a local snapshot for what was sent, and latestText.current === snapshot to choose between "saved" and "dirty" [8].
The part worth understanding is where the write happens. useLatest is useRef plus an isomorphic layout effect assigning ref.current = value keyed on [value] [9]. react-use and ahooks assign during render instead, which the post concedes works almost all the time [10]. The failure case is concurrent rendering: React's rules require render to be pure and forbid reading or writing ref.current during it, because a render can be started, paused and thrown away without committing, leaving a ref holding a value no committed UI ever showed [11]. A layout effect updates the ref exactly once per committed render, synchronously after the DOM update and before the browser paints [12]. A passive useEffect is not equivalent, per the post: passive effects run after paint, and within a commit React runs children's effects before parents' and earlier hooks before later ones, so another effect in the same commit can read the ref and get the previous render's value [14]. The post says the same trick underpins the React useEvent RFC, and that @reactuses/core builds useEvent, useInterval, useTimeoutFn and a dozen other hooks on top of useLatest [13], which puts roughly fifteen hooks on five lines of code [16].
Two things to watch before adopting it. This is a single post advocating a specific npm package, installed with npm install @reactuses/core [15][17], and at five lines the pattern is small enough to vendor rather than take as a dependency [6]. The excerpt also promises a comparison with useRef, useEvent and React's useEffectEvent plus "the one rule you must respect" [18], then cuts off before stating that rule [19]. The mechanical fix is cheap; the review question in your own codebase is which reads want then and which want now [20].
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.
In the post's autosave example, save() sets status to "saving", awaits api.save(docId, text) which takes 800 ms, then sets status to "saved"; the user types three more words while the request is in flight, the promise resolves, status flips to "saved", and the three new words are not saved.
To decide between "saved" and "dirty" after the await, the code needs to know what text is now, and the closure cannot tell you.
useLatest from @reactuses/core is that ref packaged: five lines, no re-renders, no dependency arrays.
useLatest(value) returns a MutableRefObject<T> whose .current is always the most recently rendered value; the ref object itself never changes identity, so it is safe to close over anywhere, including timers, promises and subscriptions.
The full implementation is useRef(value) plus useIsomorphicLayoutEffect(() => { ref.current = value; }, [value]), returning the ref.
Writing in useLayoutEffect means the ref updates exactly once per committed render, synchronously after the DOM is updated and before the browser paints.
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-verifying mechanism, single unaudited source
The core technical claims are demonstrated in-line: the failing and fixed component code, the complete hook source, and an explicit argument for the layout-effect write over render-body assignment or a passive effect. A reader can check that reasoning against React's documented render/commit model without leaving the post. What is missing is anything external: no link or quote from the React FAQ or useEvent RFC it invokes, no repository or version reference for @reactuses/core, no independent source, and a body that truncates before delivering the rule and useEffectEvent comparison it promised.
No adoption evidence supplied
The cluster contains no release, deployment, download, dependent-project or usage disclosure for @reactuses/core or useLatest. The post mentions react-use and ahooks only to describe how they implement the pattern, and 'a dozen other hooks' refers to the package's own internals, not to users. An npm install command is distribution instruction, not evidence of uptake, so no adoption value can be scored.
Mildly overstated by framing
The mechanism is real and modestly stated; the overstatement is in packaging and framing. Calling this a five-line fix while advertising an npm install, and positioning widely used libraries (react-use, ahooks) as subtly wrong without a single reported failure under concurrent rendering, runs slightly ahead of the evidence. The technical body itself hedges honestly — render-time assignment 'works almost all the time' and stale closures are called correct JavaScript rather than a defect — which keeps the gap small rather than large.
Package-promotion incentive, disclosed by content
The post is structured to route the reader to a specific dependency: it names @reactuses/core as the fix, prints the install command, quotes that package's hook source as canonical, credits it for building useEvent, useInterval, useTimeoutFn and a dozen further hooks on the 'correct' pattern, and singles out competing libraries as doing it the less-correct way. That is a commercial-adjacent promotional incentive even though the underlying advice is generic and reproducible without the package. Nothing in the supplied material states sponsorship or the author's relationship to the package, so the incentive is inferred from the content's own shape rather than from a disclosure.
Confident on mechanism, blind on uptake
Confidence is moderate and unevenly distributed. The claims about closure capture, the hook's source, and effect ordering are well specified and internally consistent, so the mechanism can be trusted at roughly the level of a competent tutorial. Everything about significance — how widely the package or pattern is used, whether the concurrent-rendering hazard bites in practice, and what the withheld 'one rule' qualifies — is unsupported, and the single truncated promotional source gives no way to triangulate.
build
The Escape-key listener you cannot fix with a dependency array1 distinct publisher
build
The React counter that sticks at 1, and why no dependency array will fix it1 distinct publisher
build
Your half-finished checkout form does not belong in localStorage1 distinct publisher
build
Flue 2 bets that agents are a rendering problem, not an orchestration one1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026