The data-fetching state machine behind React Query / TanStack Query, made interactive. Watch the difference between
isLoadingandisFetching, fresh vs stale, a cache hit on remount, a background refetch that keeps showing old data, and retry on error — live.
▶ Live demo · isLoading vs isFetching · Run locally
React Query is everywhere, and almost everyone is fuzzy on the same things: why does isLoading stay false during a refetch? what's the difference between stale and garbage-collected? why does my data still show while it's reloading? This is a faithful little model of TanStack Query's state machine you can poke until those click.
TanStack Query splits fetch state into two independent things:
status— about the data:pending(no data yet) →success/error.fetchStatus— about the request:fetching/idle.
That's why isLoading (≈ status === 'pending') is only true on the very first load, while isFetching (fetchStatus === 'fetching') is true on every fetch, including background refetches. Refetch a query that already has data and you'll see isFetching: true while isPending: false — so the old data stays on screen with no loading flicker. The demo makes that split visible with live flags.
- First load →
isPending, a loading skeleton, no data. - Refetch with data →
isFetchingtrue, data stays put (the "no flicker" magic). - Fresh → stale → after
staleTime, the cached data is marked stale and a trigger will refetch it. - Unmount → remount within
gcTime→ instant cache hit (plus a background refetch if stale). Wait pastgcTime→ the cache is garbage-collected and the next mount loads from scratch. - Make a fetch fail → a first load shows the error + a retry button; a background refetch error keeps the stale data instead of blanking the screen.
Every transition is logged with a timestamp.
A tiny module-level cache + a fake fetcher reproduce TanStack Query's behaviour: status/fetchStatus are tracked separately, the cache survives unmount for gcTime, and the transition log is kept in an external store read with useSyncExternalStore so logging never re-renders the query view it's measuring. No data-fetching library — the point is the mechanics.
npm install
npm run dev
npm run buildIdeas: a second query sharing the cache key (dedup), placeholderData/keepPreviousData, retry-with-backoff, window-focus refetch. PRs welcome.
If this finally separated isLoading from isFetching for you, a ⭐ helps others find it.
MIT © dev48v