React Query Showing Stale Data? Causes and Fixes (v5)
React Query showing old data on first render? Fix initialData treated as fresh, incomplete query keys, and v4 leftovers like keepPreviousData in TanStack Query v5.

React Query shows stale data when the cache already holds an entry that it considers fresh, so it renders that entry and skips the fetch. In TanStack Query v5 the three most common causes are initialData combined with staleTime, a query key that does not include every variable the request depends on, and caching options carried over from v4. Navigating away and back often hides the bug, because the component remounts after the entry has gone stale.
Quick Diagnosis
| Symptom | Likely cause | Fix |
|---|---|---|
| Empty or default data on first render, correct after navigation | initialData plus staleTime marks the seed data as fresh | Use placeholderData, or set initialDataUpdatedAt |
| Data from the previous user, page, or filter | Query key missing a dependency | Put every input of queryFn in queryKey |
| Old data after a mutation | Cache not invalidated | invalidateQueries with the key prefix |
| Table flashes empty while paginating after upgrading to v5 | keepPreviousData option was removed in v5 | placeholderData: keepPreviousData |
| Data never refreshes when returning to the tab | staleTime too long or refetchOnWindowFocus disabled | Lower staleTime or refetch on focus |
Why Does React Query Show Old Data on the First Render?
Staleness is decided per cache entry. staleTime defines how long an entry counts as fresh, and its default is 0, so data is stale right after it arrives unless you raise it. When a component mounts, React Query refetches only if the entry is stale. Anything that makes a wrong entry look fresh gets rendered as is.
This is the exact bug I hit in a paginated dashboard. I seeded the query with an empty initialData object so the table had something to render, and I added a five minute staleTime to cut down on requests. The first load showed an empty table. Navigating away and coming back showed the real rows.
The cause is that initialData is written into the cache and treated like data that was just fetched. With staleTime set to five minutes, React Query believed the empty object was fresh and had no reason to call the API.
Fix 1: Use placeholderData for Loading States, Not initialData
placeholderData renders while the query loads but is never persisted to the cache, so it cannot block the real fetch. The isPlaceholderData flag tells you when it is on screen.
import { useQuery } from "@tanstack/react-query";
const emptyPage = { currentPage: 0, totalItems: 0, totalPages: 0, data: [] };
const { data, isPlaceholderData } = useQuery({
queryKey: ["dataList", { userId, page, pageSize }],
queryFn: () => fetchData({ userId, page, pageSize }),
placeholderData: emptyPage,
staleTime: 1000 * 60 * 5,
});If you really need initialData, for example data rendered on the server, tell React Query how old it is with initialDataUpdatedAt. An old timestamp, or 0, marks the entry as stale so the query refetches on mount.
useQuery({
queryKey: ["dataList", { userId, page, pageSize }],
queryFn: () => fetchData({ userId, page, pageSize }),
initialData: serverData,
initialDataUpdatedAt: serverFetchedAt, // use 0 to always refetch on mount
staleTime: 1000 * 60 * 5,
});Fix 2: Include Every Dependency in the Query Key
The query key is the cache address. If queryFn reads userId, page, and pageSize but the key is only [“dataList”], every user and page shares one cache entry, and you see whichever response landed last.
// Wrong: page 2 reuses the cache entry from page 1
useQuery({ queryKey: ["dataList"], queryFn: () => fetchData({ userId, page }) });
// Right: each combination gets its own entry
useQuery({
queryKey: ["dataList", { userId, page, pageSize }],
queryFn: () => fetchData({ userId, page, pageSize }),
});The official TanStack Query ESLint plugin ships an exhaustive-deps rule for query keys that catches this at lint time.
Fix 3: Replace keepPreviousData With placeholderData in v5
TanStack Query v5 removed the keepPreviousData option and the isPreviousData flag. According to the official v5 migration guide, the same behavior now comes from passing the keepPreviousData helper to placeholderData.
import { keepPreviousData, useQuery } from "@tanstack/react-query";
const { data, isPlaceholderData } = useQuery({
queryKey: ["dataList", { userId, page, pageSize }],
queryFn: () => fetchData({ userId, page, pageSize }),
placeholderData: keepPreviousData,
});One behavior change matters here. With placeholderData the query stays in a success state while the next page loads, while v4 keepPreviousData could expose an error status from the previous query. Use isPlaceholderData to dim the table or disable the next page button.
Fix 4: Invalidate After Mutations
A mutation changes data on the server, but the cache does not know that. Invalidate the affected keys when the mutation succeeds. In v5, invalidateQueries takes an object, and a key prefix matches every query that starts with it.
import { useMutation, useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
const updateItem = useMutation({
mutationFn: saveItem,
onSuccess: () => {
// Marks every ["dataList", ...] query stale and refetches the active ones
queryClient.invalidateQueries({ queryKey: ["dataList"] });
},
});Avoid calling invalidateQueries inside a useEffect that runs whenever the key changes. A new key already triggers a fetch, so the effect only adds duplicate requests.
staleTime vs gcTime: What Is the Difference?
staleTime controls when data needs refetching. gcTime, called cacheTime before v5, controls when unused data is removed from memory.
| Option | Default | What it controls | Raise it when |
|---|---|---|---|
| staleTime | 0 | How long data counts as fresh, so no refetch on mount or window focus | Data rarely changes and you want fewer requests |
| gcTime | 5 minutes | How long an unused entry stays cached after its last observer unmounts | Users often return to the same screen |
A high staleTime never makes data fresher. It does the opposite, it tells React Query to trust the cache for longer.
Fix 5: Control When Refetches Happen
If a screen must always show current data, force a refetch on mount regardless of staleTime, and refetch when the user returns to the tab.
useQuery({
queryKey: ["dataList", { userId, page, pageSize }],
queryFn: () => fetchData({ userId, page, pageSize }),
refetchOnMount: "always",
refetchOnWindowFocus: true,
});For dashboards that need to stay live, add refetchInterval with a value in milliseconds.
Also Read: Zustand: Lightweight State Management for Modern React Apps
How to Confirm the Cause With React Query Devtools
Open React Query Devtools and select the query. Three details answer most stale data questions:
- The query key. If two screens show the same key, they share one cache entry.
- The status badge. A fresh badge on data you know is wrong points to initialData plus staleTime.
- The last updated time. A timestamp from before the page loaded means the data came from cache, not a new request.
Also Read: Server Actions vs Client Rendering in Next.js: The 2025 Guide
Frequently Asked Questions
Why does navigating away and back fix stale data in React Query?
Remounting runs the refetch on mount check again, and by then the entry may have gone stale, so a fetch happens. It hides the real cause, which is usually initialData treated as fresh or a query key missing a dependency.
Is staleTime 0 by default in TanStack Query v5?
Yes. staleTime defaults to 0 and gcTime defaults to 5 minutes, so data is considered stale right after it is fetched unless you configure otherwise.
What replaced keepPreviousData in TanStack Query v5?
Import the keepPreviousData helper from @tanstack/react-query and pass it to placeholderData. The isPreviousData flag became isPlaceholderData.
Does initialData trigger a fetch on mount?
Only when it is stale. initialData is stored in the cache, so with a non zero staleTime it counts as fresh and blocks the fetch, unless initialDataUpdatedAt says it is older than staleTime.
Checklist Before You Ship
- Loading states use placeholderData, not initialData.
- Every value used by queryFn appears in queryKey.
- Mutations invalidate the key prefixes they affect.
- v4 options are migrated: cacheTime to gcTime, keepPreviousData to placeholderData.
- staleTime matches how often the data really changes.


