async-effect-race-contracts

Installation
SKILL.md

Async effect race contracts

A raw useEffect that touches the outside world (fetch, subscribe, listen, schedule) owes two things the happy path never shows: a take-latest guard so an older async result cannot overwrite a newer one, and a cleanup that mirrors setup exactly so nothing keeps running after the effect re-runs or the component unmounts.

Checklist (lead with the trap)

  1. Take-latest, not last-response-wins: guard every fetch-on-deps effect. Per-run let ignore = false flipped in cleanup and checked before setState, or an AbortController aborted in cleanup. ignore only discards the result; AbortController actually cancels the request — swallow its AbortError, do not surface it as a real error.
  2. Every setup that subscribes/opens/schedules needs a cleanup that undoes itsubscribeunsubscribe, addEventListenerremoveEventListener (same function reference), setInterval/setTimeoutclear*, connectdisconnect. A missing return accumulates a new listener/interval/connection on every re-run — that is the real leak; a late async setState after unmount is harmless by itself, but React 18 removed its warning, so the console no longer flags either case.
  3. StrictMode double-invoke is a stress test, not a bug — make setup idempotent, do not suppress it. Blocking the second run with a useRef guard hides the bug — the real remount on navigate-away-and-back still leaks. A side effect that should not fire on display at all (a POST, a registration) belongs in an event handler, not an effect.
  4. Stale closure: a value read inside a long-lived callback is frozen at the render that created it. An interval/timeout/subscription/event handler set up once ([]) captures that render's props and state forever, so it keeps reading the old value. In order of preference: a functional updater setX(x => ...) when you only need previous state; useEffectEvent to read the latest reactive value without restarting the timer (it is non-reactive, must be omitted from deps, and may only be called from inside an effect); or a ref you keep current (ref.current) on React without useEffectEvent. Adding the value to deps also works but restarts the timer on every change.
  5. Dependency array: omissions read stale; unstable references loop. Fix by moving the declaration inside the effect, wrapping the reference in useMemo/useCallback, or using a functional updater. Do not silence react-hooks/exhaustive-deps with a disable comment — a suppressed dep is where these bugs hide.
  6. [], no array, and [a, b] mean different things — confirm intent matches the effect. An effect with [] that reads a prop or state is a stale closure waiting to happen — pick a fix from item 4 rather than lying about the deps.

Quick probes

Treat hits as leads; open each effect and trace setup -> cleanup -> dependency array.

Installs
1
GitHub Stars
1
First Seen
3 days ago
async-effect-race-contracts — voidmatcha/frontend-niche-skills