Making Sense of React Query: How We Tamed Data-Fetching at CosX

Making Sense of React Query: How We Tamed Data-Fetching at CosX
How We Tamed Data-Fetching at CosX
Written by Bishakh Neogi

At CosX, we built a platform where performance and responsiveness were non-negotiable. Our components often dealt with live job datacomplex filters, and multiple data views switching rapidly.

Initially, we leaned on traditional data fetching, managing loading states withuseState, tracking lifecycle withuseEffect, and juggling isLoading across component boundaries. But it became fragile fast.

So we adopted React Query, and it changed everything.

But React Query brought its learning curve. Especially around:

1. isLoading

2. isFetching

3. staleTime

4. gcTime

This post is your guide through these concepts, told as a story of how we used them, broke them, and finally understood them.

Why React Query?

React Query (now part of TanStack Query) is a headless data-fetching and caching library. It makes:

  • fetching data,
  • caching it,
  • invalidating it,
  • refetching it in the background,
  • and managing pagination

… feel like magic.

It abstracts away boilerplate while giving you fine-grained control over how and when your data should behave.

Code snippet zone:

Firstly, a query hook:

export const useGetScheduledJobsQuery = (
  size: number = 50,
  searchParam?: string,
  options?: { staleTime?: number; gcTime?: number }
) => {
  return useQuery({
    queryKey: ["scheduledJobs", size, searchParam],
    queryFn: () => getScheduledJobs({ size, searchParam }),
    staleTime: options?.staleTime,
    gcTime: options?.gcTime,
  });
};

Two different components — ComponentA and ComponentB — used the same data, with users switching between them often.

const { data, isLoading, isFetching } = useGetScheduledJobsQuery(50, "apple", {
  staleTime: STALE_TIME_SETTING,
  gcTime: GC_TIME_SETTING,
});

And that’s where we ran into confusion.

What’s the Difference Between isLoading and isFetching?

  1. isLoading: The first fetch, and no cached data exists
  2. isFetchingAny fetch in progress, including background refetches

In other words, isLoading=true is the cold start and isFetching=true means the server request is happening (might be background).

What is staleTime and gcTime?

  1. staleTime: How long cached data is considered fresh. By default the staleTime is 0 seconds.
  2. gcTime: How long react-query keeps unused data in memory (cache memory). By default the gcTime is 5mins.

You can have any of the 3 scenarios:

a. fresh but garbage-collected data (gcTime < staleTime)

b. stale but still in memory (staleTime < gcTime)

c. infinite everything, or nothing cached at all.

Simplification of the working:

1. gcTime < staleTime:

At 12:00, Component A mounts for the first time. React Query checks its cache — it’s empty. So a server request is made. Both isLoading and isFetching are true, and the user sees a spinner.

By 12:01, the data comes back. React Query caches it and updates the UI. isLoading flips to false — data is shown, life is good.

At 12:02, the user switches to Component B. Since the data is still in cache, React Query doesn’t show a loading state — isLoading: false, andisFetching: true.

At 12:04, the user switches to Component C, which does not share the hook. Thus, components A and B become inactive, removing the data from the cache.

Now at 12:05, the user switches back to A. But enough time has passed that React Query has garbage-collected the cache. So it no longer has data. A new fetch is triggered, and once again, isLoading: trueisFetching: true.

2. gcTime=staleTime:

At 12:00, Component A mounts. There’s no cached data, so React Query makes a fresh request to the server. isLoading And isFetching are both true — your UI shows a full loader.

At 12:01, the data is fetched and displayed. But here’s the catch: becausestaleTime = 0it's instantly considered stale. And since gcTime = 0 too, the cached data is removed as soon as the component unmounts.

At 12:02, the user switches to Component B.

By 12:03, React Query has no cache to work with, so it behaves as if it’s seeing this query for the first time again. Another cold fetch. isLoading and isFetching are both true.

This pattern keeps repeating, causing redundant server calls and constant loading states.

This setup is great for always-live data, but creates a choppy UX for most apps.

3. The Infinity Trick: When Both staleTime and gcTime Are Set to Infinity

At 12:00, Component A mounts with no cache. React Query triggers a server call. Both isLoading and isFetching are true.

By 12:01, the data is fetched, cached, and displayed. Since staleTime = Infinity, the data is considered fresh forever. And because gcTime = Infinity, React Query never deletes this cache — even after unmounting.

At 12:03, the user switches to Component B. React Query finds fresh data in memory. It doesn’t show a loader. isLoading = falseisFetching = false unless manually refetched.

Even at 12:05, when the user returns to Component A, no refetch occurs. Cached data is served immediately — smooth, instant UX.

So, What Did We Learn from This at CosX?

As we navigated the maze of data fetching at CosX, we uncovered a few golden rules — not just technical tweaks, but lessons in user experience design.

We noticed the flickers. Every time a user switched tabs or moved between components, the screen blinked with loading spinners. It felt cheap. The fix?
1. We gave the cache more breathing room by setting a higher gcTime — or even Infinity.

We saw the network traffic pile up, with React Query refetching on every mount. Users didn’t notice, but our infra did. The fix?
2. We introduced a smart staleTime window, telling React Query: “Chill, the data’s still fresh.”

Then came the scenarios where we wanted complete control. For static metadata or infrequently changing lists, we wanted data to stay forever unless we decided otherwise. The fix?
3. We went all in with gcTime = Infinity and staleTime = Infinity.

If you’re using React Query in a real-world app:

Think like the cache.
Design your data strategy intentionally.
Let caching be a feature, not an accident.