Skip to main content

TanStack Query Cache: How React Data Persists

TanStack Query's cache is an in-memory store that holds fetched data for reuse across components. Every time you run a query, the library checks the cache first; if a matching key exists and the data is fresh (not stale), it returns the cached value without an HTTP request. Understanding the cache lifecycle—when data is fresh, when it becomes stale, and when it is discarded—is essential to avoiding unnecessary requests and building responsive applications. This article covers the two cache timers that control everything: staleTime and gcTime.

I once shipped a feature where user settings changed on the backend but the frontend cache never refreshed because I set staleTime to one hour. Users would update their preferences and see no change for 60 minutes. Learning to tune staleTime and gcTime separately prevented that class of bug ever again.

The Two Cache Timers: staleTime and gcTime

TanStack Query uses two independent timers per cache entry:

staleTime — How long cached data is considered fresh. While staleTime is active, the library returns cached data without refetching, even if the component remounts or the hook is called again. Default is 0 seconds (immediately stale).

gcTime — How long the library keeps cached data in memory after all components using it unmount. Once this timer expires, the cache entry is discarded. Default is 5 minutes. (Formerly called cacheTime; renamed in v5.)

const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 1000 * 60 * 5, // Fresh for 5 minutes
gcTime: 1000 * 60 * 10, // Keep in memory for 10 minutes
});

Here is the timeline:

  1. Fetch runs at time 0. Data is fetched and cached.
  2. Until staleTime elapses (5 min): library returns cached data instantly, no refetch.
  3. After staleTime elapses (5 min): data is marked stale. Next component mount or explicit call to useQuery triggers a background refetch.
  4. After component unmounts (any time): cache entry stays in memory for gcTime (10 min).
  5. After gcTime elapses (10 min): cache entry is deleted from memory.

The key insight: staleTime controls when the library re-fetches in the background; gcTime controls when the data is thrown away entirely.

Choosing staleTime for Your Data

Set staleTime based on your data's volatility:

Data TypeExampleSuggested staleTime
User settingsProfile, preferences5–10 minutes
Product catalogStore inventory30–60 seconds
Real-time metricsServer stats, analytics0–5 seconds
Mostly staticBlog posts, documentation30–60 minutes
User-generated contentComments, posts10–30 seconds

For example, a product catalog that updates rarely can have a long staleTime:

const { data: products } = useQuery({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('/api/products');
return res.json();
},
staleTime: 1000 * 60 * 30, // 30 minutes; user won't see new products until 30 min has passed
});

Whereas a real-time stock ticker needs frequent refetches:

const { data: stocks } = useQuery({
queryKey: ['stocks'],
queryFn: async () => {
const res = await fetch('/api/stocks');
return res.json();
},
staleTime: 1000 * 5, // 5 seconds
});

A conservative approach for most UI data: staleTime: 1000 * 60 (1 minute). This balances freshness with reduced requests.

Understanding Garbage Collection (gcTime)

gcTime controls how long cached data persists after no components are using it. This is useful for scenarios where a user navigates away from a page but might return:

const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 1000 * 60 * 5, // Fresh for 5 minutes
gcTime: 1000 * 60 * 10, // Keep for 10 minutes after unmount
});

// If user navigates to /profile, then back to /profile within 10 minutes:
// The second render instant-hits the cache instead of refetching

If you set gcTime: 0, the cache is purged immediately when the last component unmounts, forcing a fresh fetch if the component remounts. This is useful for sensitive data:

const { data: bankAccount } = useQuery({
queryKey: ['bankAccount'],
queryFn: fetchBankAccount,
staleTime: 1000 * 30, // 30 seconds
gcTime: 0, // Delete from cache on unmount
});

Background Refetching and the Stale-While-Revalidate Pattern

When staleTime expires, the library marks data as stale but does NOT immediately discard it. Instead, it triggers a background refetch the next time the query is accessed (a component mounts or useQuery is called). During this refetch, old data is displayed while new data is fetched in the background. This is called "stale-while-revalidate" and is a web caching best practice (RFC 5861).

function UserProfile({ userId }) {
const { data: user, isFetching } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 1000 * 60 * 5,
});

return (
<div>
<h1>{user?.name}</h1>
{isFetching && <p>Refreshing user data...</p>}
</div>
);
}

// If 5 minutes have passed and a component remounts:
// 1. Old data is rendered instantly from cache
// 2. A refetch happens in the background
// 3. isFetching becomes true while new data arrives
// 4. UI updates with fresh data

This pattern feels instant to users (no loading spinner) while ensuring data eventually becomes fresh.

Controlling Refetch Behavior

You can override automatic refetching with these options:

const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
refetchOnMount: true, // Refetch on component mount (default: true)
refetchOnWindowFocus: true, // Refetch when window regains focus
refetchOnReconnect: true, // Refetch when network reconnects
refetchInterval: 1000 * 60, // Refetch every minute
refetchIntervalInBackground: false, // Don't refetch if tab is hidden
});
  • refetchOnMount: true refetches if data is stale when a new component mounts.
  • refetchOnWindowFocus: true refetches if data is stale when the user switches back to your tab.
  • refetchInterval sets a polling interval, useful for time-series data or real-time dashboards.
  • refetchIntervalInBackground: false pauses polling if the user is not actively viewing the page (battery savings).

Cache Invalidation and Manual Refetch

Override the cache by manually invalidating entries:

const queryClient = useQueryClient();

// Invalidate a specific user query
queryClient.invalidateQueries({
queryKey: ['user', userId],
exact: true,
});

// Invalidate all user queries
queryClient.invalidateQueries({
queryKey: ['user'],
exact: false,
});

// Force a refetch even if data is still fresh
queryClient.refetchQueries({
queryKey: ['user', userId],
});

invalidateQueries marks the cache entry as stale, triggering a background refetch the next time the query is accessed. refetchQueries immediately re-runs the fetcher without waiting for a component to mount.

Real-World Cache Configuration Example

Here is a typical configuration for a dashboard with multiple data types:

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute default
gcTime: 1000 * 60 * 5, // 5 minutes default
retry: 1,
refetchOnWindowFocus: true,
},
},
});

// Override for user settings (stable, refresh rarely)
const { data: settings } = useQuery({
queryKey: ['userSettings'],
queryFn: fetchSettings,
staleTime: 1000 * 60 * 60, // 1 hour
});

// Override for notifications (volatile, fetch frequently)
const { data: notifications } = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 1000 * 10, // 10 seconds
refetchInterval: 1000 * 30, // Poll every 30 seconds
});

By setting sensible defaults and overriding per query, you keep code DRY while tuning freshness per resource.

Key Takeaways

  • staleTime controls how long cached data is considered fresh; fresh data is returned without refetching.
  • gcTime controls how long data persists in memory after all components unmount; expired data is deleted.
  • Use stale-while-revalidate: once staleTime expires, data is refetched in the background while old data is displayed.
  • Set staleTime based on data volatility: 1–5 minutes for most UI data, seconds for real-time, hours for static content.
  • refetchOnWindowFocus and refetchOnReconnect are powerful for ensuring fresh data when the user returns after network loss.
  • Use invalidateQueries after mutations to mark cache entries stale, triggering automatic refetches.

Frequently Asked Questions

What is the difference between staleTime and refetchInterval?

staleTime makes data considered fresh for a duration; once it expires, the next query access triggers a refetch. refetchInterval sets up a polling timer that re-runs the fetcher at fixed intervals regardless of staleness. Use staleTime to reduce requests; use refetchInterval for real-time data that needs continuous polling.

If I set staleTime to 0, doesn't the data refetch every time?

No. If staleTime: 0, data is immediately marked stale, but it is not refetched until a component remounts or useQuery is called again. If a component stays mounted, the cache is reused. To force constant polling, use refetchInterval.

Can I set different staleTime for the same query key?

No, the first useQuery call wins. If two components call useQuery with the same key but different staleTime values, the first call's setting is used globally. To work around this, use different keys for different freshness requirements, or manage freshness via mutations and invalidateQueries.

Does gcTime affect how long data is stored on disk (localStorage)?

No, TanStack Query's cache is in-memory only by default. gcTime controls RAM-only data. To persist cache to localStorage or a database, use community libraries like @tanstack/react-query-persist-client or implement custom serialization.

What happens if a refetch fails while displaying stale data?

The old stale data remains displayed, and isError becomes true. The user sees the old data with an error indicator. The library will retry the refetch based on your retry configuration. Use this to build resilient UIs: show the stale data, and let the user know it might not be current.

Further Reading