Web Development

React Query Showing Stale Data? Causes and Fixes (v5)

Asep Alazhari

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 Showing Stale Data? Causes and Fixes (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

SymptomLikely causeFix
Empty or default data on first render, correct after navigationinitialData plus staleTime marks the seed data as freshUse placeholderData, or set initialDataUpdatedAt
Data from the previous user, page, or filterQuery key missing a dependencyPut every input of queryFn in queryKey
Old data after a mutationCache not invalidatedinvalidateQueries with the key prefix
Table flashes empty while paginating after upgrading to v5keepPreviousData option was removed in v5placeholderData: keepPreviousData
Data never refreshes when returning to the tabstaleTime too long or refetchOnWindowFocus disabledLower 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.

OptionDefaultWhat it controlsRaise it when
staleTime0How long data counts as fresh, so no refetch on mount or window focusData rarely changes and you want fewer requests
gcTime5 minutesHow long an unused entry stays cached after its last observer unmountsUsers 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:

  1. The query key. If two screens show the same key, they share one cache entry.
  2. The status badge. A fresh badge on data you know is wrong points to initialData plus staleTime.
  3. 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.
Back to Blog

Related Posts

View All Posts »