DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

isLoading vs isFetching: I Built an Interactive Model of the React Query State Machine

React Query (TanStack Query) is everywhere, and almost everyone is fuzzy on the same three things: why does isLoading stay false during a refetch? what's the difference between "stale" and "garbage-collected"? why does my old data stay on screen while it reloads? So I built an interactive model of the state machine you can poke until it clicks.

▶ Live demo: https://react-query-states.vercel.app/
Source (React 19 + TS, no fetching library): https://github.com/dev48v/react-query-states

The split that explains everything: status vs fetchStatus

TanStack Query tracks fetch state on two independent axes:

  • status is about the data: pending (no data yet) → success / error.
  • fetchStatus is about the request: fetching / idle.

Once you see them as separate, the confusing flags fall out:

isLoading  ≈ status === 'pending'      → true ONLY on the first load (no data yet)
isFetching = fetchStatus === 'fetching' → true on EVERY fetch, including background refetches
Enter fullscreen mode Exit fullscreen mode

So when you refetch a query that already has data: isFetching flips to true, but isPending stays false — which is why the old data stays on screen with no loading flicker. That's not a bug, it's the whole point. The demo shows both flags live as you click refetch.

Fresh → stale → garbage-collected

Two timers people mix up:

  • staleTime — how long after a successful fetch the data is considered fresh. While fresh, React Query won't refetch on mount/focus. After it, the data is stale and a trigger (mount, refetch, window focus) will revalidate in the background.
  • gcTime (formerly cacheTime) — how long the cache survives after the last observer unmounts. Unmount and remount within gcTime → instant cache hit. Wait past it → the entry is garbage-collected and the next mount loads from scratch.

The demo lets you unmount/remount and watch the cache hit (or miss, after gcTime).

Errors don't always blank the screen

A first load failure → status: 'error', show the error + a retry. But a background refetch failure keeps the last good data on screen (you just get an error alongside the stale data). The demo has a "next fetch fails" toggle so you can see both paths.

How it's built

A tiny module-level cache + a fake fetcher reproduce the behaviour — status/fetchStatus tracked separately, cache surviving unmount for gcTime, background refetch keeping stale data. The transition log lives in an external store (useSyncExternalStore) so logging never re-renders the query view it's measuring. No data-fetching library; the point is the mechanics.

If this finally separated isLoading from isFetching for you, a star helps others find it: https://github.com/dev48v/react-query-states

Top comments (0)