← All posts

React useLatest: fresh state in async callbacks without extra renders

useLatest from @reactuses/core: a layout-effect ref against stale closures after await, timers, and subscriptions.

React useLatest: fresh state in async callbacks without extra renders
Contents

In brief

A Save button flips to saved while the user keeps typing: the closure still holds the old text. A Dev.to post walks through useLatest from @reactuses/core — a ref updated in a layout effect so callbacks after await, timers, and subscriptions can read the last committed value.

What happened

Any callback that outlives its render (await, setTimeout, a listener, an SDK) sees the props/state of the render that created it. That is a normal closure; it is a bug only when you needed now and got then.

useLatest(value) returns a stable ref: .current updates in an isomorphic layout effect, not during render (concurrent React can discard a render). In the autosave example the closure holds the snapshot to send; the ref holds text after the API returns, so you can choose saved vs dirty.

Neighbours: useEvent / useEffectEvent wrap functions; useLatest wraps values. Typical spots: search races in handlers, a map/WebSocket with empty deps, a toast that pauses on hover.

Why it matters

Putting state in effect deps often means tearing down an expensive subscription. A ref keeps an honest empty deps array and fresh reads inside. Reading .current in JSX is wrong — the UI needs useState.

In practice

  1. After await, compare the sent snapshot to latest.current.
  2. One-shot subscriptions should read through a ref, not deps that rebuild the client.
  3. Write the ref in a layout effect, not in the render body.
  4. Function for a child/effect → useEvent; value for a callback → useLatest.
  5. Don’t put the ref in deps “to react” — its identity is stable.

Takeaway

useLatest is a small pattern against stale closures: always current from callbacks, never triggers a re-render, never changes identity. For on-screen values, still use state.