Skip to content

Instantly share code, notes, and snippets.

@4sskick
Last active September 2, 2026 09:49
Show Gist options
  • Select an option

  • Save 4sskick/4f9df6ae3d38c498c1c115af0fb2fca4 to your computer and use it in GitHub Desktop.

Select an option

Save 4sskick/4f9df6ae3d38c498c1c115af0fb2fca4 to your computer and use it in GitHub Desktop.
Ever wondered why React Query seems to show **two different values** when your API only returned one? I recently hit a nasty bug where the UI showed the **old popup first, then the correct one**. Turns out, the problem wasn’t the API. It was **stale cached data**. Here’s what actually happened — and why `gcTime: 0` fixed it.

React Query Showed Me the Wrong Popup — Then the Correct One

Ever had a React app do something that makes you think:

“Why the hell did this code run twice?”

That happened to me with React Query.

I had a screen that fetches a server-side rule and decides which popup to show:

  • PASSWORD → show password popup
  • EMAIL_OTP → show email OTP popup

Pretty straightforward.

const { data: securityRule } = useFetchSecurityRule(countryCode);

if (securityRule === 'PASSWORD') {
  showPasswordPopup();
}

if (securityRule === 'EMAIL_OTP') {
  showEmailPopup();
}

Then I found a very strange bug.

The Bug

First visit:

Server → PASSWORD
App    → Password popup

I leave the screen.

Then I change the backend configuration:

PASSWORD → EMAIL_OTP

I open the screen again.

And suddenly...

Password popup
        ↓
   Email OTP popup

Both appeared.

The weird part?

The server only returned one answer on the second request.

So why did my UI react to two different values?

The Culprit: React Query Cache

The problem wasn't that React Query fetched the data twice in the way I initially thought.

It was doing exactly what it was designed to do:

serve cached data immediately, then refresh it in the background.

The timeline looked like this:

FIRST VISIT

Server
  │
  └── PASSWORD
         │
         ▼
   React Query Cache

I leave the screen.

The cached data is still kept in memory.

Then I change the backend:

PASSWORD → EMAIL_OTP

When I visit the screen again:

Screen opens
     │
     ├── Cached value → PASSWORD
     │                    ↓
     │              Password popup
     │
     └── Background fetch
              │
              ▼
          EMAIL_OTP
              │
              ▼
        Email OTP popup

So my application effectively processed:

PASSWORD
   ↓
EMAIL_OTP

within a very short period.

React Query wasn't broken.

My assumption about the cache behavior was.

The One-Line Fix

For this particular query, I don't want React Query to keep the previous answer around.

So I changed it to:

useQuery({
  queryKey: ['SECURITY_RULE', countryCode],
  queryFn: fetchSecurityRule,
  gcTime: 0,
  onSuccess: (rule) => {
    // show popup
  },
});

That one line:

gcTime: 0

fixed the double-popup behavior.

But Why gcTime?

This is where I initially got confused too.

There are two settings people often mix up:

staleTime

Think:

“How long is this data considered fresh?”

With the default staleTime: 0, data becomes stale immediately.

gcTime

Think:

“How long should React Query keep this data after nobody is using it?”

The default is 5 minutes.

So even after leaving the screen, React Query can still have the old result sitting in its cache.

That's exactly what happened to me.

Why gcTime: 0 Works

With:

gcTime: 0

I'm effectively telling React Query:

“Don't keep this query around after the screen is gone.”

So when I revisit the screen:

No cached PASSWORD
        ↓
Fetch server
        ↓
EMAIL_OTP
        ↓
Show one popup

No stale value gets a chance to trigger the UI first.

The Important Lesson

React Query's cache is great when stale data is acceptable.

For example:

  • feeds
  • product lists
  • profiles
  • dashboards

Showing something old for a moment while fetching the latest data usually makes the app feel faster.

But that's very different from data that controls application flow.

Be careful when a query determines:

  • which authentication flow to use
  • which popup to show
  • whether a user should be redirected
  • security-related decisions
  • other critical UI state

In those cases, displaying stale data first can produce some very confusing bugs.

My rule of thumb

If the data is:

“Nice to have immediately”

→ caching is probably great.

If the data is:

“Must be correct before I continue”

→ think very carefully about caching and stale data.

And when the query should not survive after leaving the screen:

gcTime: 0

can be a surprisingly effective solution.


The biggest takeaway: React Query wasn't showing me two server responses.

It was showing me one cached response + one fresh response.

And that tiny distinction completely changed how I debugged the bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment