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 popupEMAIL_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.
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 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.
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: 0fixed the double-popup behavior.
This is where I initially got confused too.
There are two settings people often mix up:
Think:
“How long is this data considered fresh?”
With the default staleTime: 0, data becomes stale immediately.
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.
With:
gcTime: 0I'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.
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.
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: 0can 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.