TanStack Query provides powerful caching mechanisms that automatically manage the lifecycle of your data. Understanding how caching works is essential to building efficient applications.
Cache Fundamentals
Every query result is cached using the queryKey as a unique identifier. The cache stores both successful data and error states, allowing TanStack Query to provide instant responses to subsequent requests.
import { useQuery } from '@tanstack/react-query'
function Posts() {
// This query result is cached with key ['posts']
const { data } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
return await response.json()
},
})
}
Stale Time
The staleTime option determines how long cached data is considered “fresh” before it becomes stale. By default, data is immediately stale (staleTime: 0).
Stale data can still be used to render your UI instantly, but TanStack Query will refetch it in the background to ensure freshness.
// Data is fresh for 5 seconds
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5000,
})
// Data never becomes stale
const { data } = useQuery({
queryKey: ['static-data'],
queryFn: fetchStaticData,
staleTime: Infinity,
})
Dynamic Stale Time
You can compute staleTime dynamically based on the query:
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: (query) => {
// Fresh for 1 minute for admin users, 5 minutes otherwise
return query.state.data?.role === 'admin' ? 60000 : 300000
},
})
Garbage Collection Time (gcTime)
The gcTime (formerly cacheTime) determines how long inactive queries remain in memory before being garbage collected. The default is 5 minutes.
Query becomes inactive
When all components using a query unmount, the query becomes inactive.
Garbage collection timer starts
A timer starts based on the gcTime value.
Cache cleanup
After the timer expires, the query data is removed from memory.
import { QueryClient } from '@tanstack/react-query'
// Set default gcTime for all queries
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
// Override for specific queries
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
gcTime: Infinity, // Never garbage collect
})
Cache Data Access
You can imperatively access cached data using getQueryData:
import { useQueryClient } from '@tanstack/react-query'
function Component() {
const queryClient = useQueryClient()
// Read from cache
const cachedPosts = queryClient.getQueryData(['posts'])
// Check if specific data is cached
const isPostCached = !!queryClient.getQueryData(['post', postId])
return (
<a
href="#"
style={{
fontWeight: isPostCached ? 'bold' : 'normal',
color: isPostCached ? 'green' : 'inherit',
}}
>
View Post
</a>
)
}
getQueryData is non-reactive. Use it for imperative operations like optimistic updates, not for rendering. Use useQuery in components to subscribe to cache changes.
Manual Cache Updates
You can manually update the cache using setQueryData:
const queryClient = useQueryClient()
// Direct update
queryClient.setQueryData(['posts'], (oldPosts) => [
...oldPosts,
newPost,
])
// Update with timestamp tracking
queryClient.setQueryData(
['posts'],
newData,
{ updatedAt: Date.now() }
)
Structural Sharing
TanStack Query uses structural sharing by default to minimize re-renders. This means that if the new data is deeply equal to the old data, the old reference is kept.
// Disable structural sharing if needed
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
structuralSharing: false,
})
// Custom structural sharing logic
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
structuralSharing: (oldData, newData) => {
// Your custom logic
return newData
},
})
Cache Persistence
You can persist the cache to local storage or other storage mechanisms:
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
const persister = createAsyncStoragePersister({
storage: window.localStorage,
})
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<YourApp />
</PersistQueryClientProvider>
)
}
Combine persistence with a long gcTime to create offline-capable applications that work seamlessly across page reloads.
Cache Strategies
Prefetch Strategy
Prefetch data before it’s needed:
const queryClient = useQueryClient()
// Prefetch on hover
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
})
}
Initial Data Strategy
Seed the cache with initial data:
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
initialData: () => {
// Find post from cached list
return queryClient
.getQueryData(['posts'])
?.find(post => post.id === postId)
},
})
Placeholder Data Strategy
Show placeholder data while loading:
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
placeholderData: (previousData, previousQuery) => {
// Use data from list as placeholder
return queryClient
.getQueryData(['posts'])
?.find(post => post.id === postId)
},
})