Documentation Index
Fetch the complete documentation index at: https://mintlify.com/TanStack/query/llms.txt
Use this file to discover all available pages before exploring further.
TanStack Query supports persisting the query cache to external storage, allowing your app to restore cached data across page reloads and sessions.
Installation
# For localStorage/sessionStorage
pnpm add @tanstack/query-sync-storage-persister
# For AsyncStorage (React Native)
pnpm add @tanstack/query-async-storage-persister
# Core persistence package (required)
pnpm add @tanstack/query-persist-client-core
Basic Setup
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<YourApp />
</PersistQueryClientProvider>
)
}
Storage Options
localStorage
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage,
key: 'TANSTACK_QUERY_CACHE', // Custom key (optional)
})
sessionStorage
const persister = createSyncStoragePersister({
storage: window.sessionStorage,
})
IndexedDB
For larger datasets:
import { experimental_createPersister } from '@tanstack/query-persist-client-core'
import { get, set, del } from 'idb-keyval'
const persister = experimental_createPersister({
storage: {
getItem: async (key) => await get(key),
setItem: async (key, value) => await set(key, value),
removeItem: async (key) => await del(key),
},
})
React Native AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
})
Persist Options
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
buster: 'v1', // Version your cache
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// Only persist successful queries
return query.state.status === 'success'
},
},
hydrateOptions: {
// Options for restoring queries
},
}}
>
<App />
</PersistQueryClientProvider>
maxAge
Cache expiration time:
persistOptions={{
persister,
maxAge: 1000 * 60 * 60 * 24, // Discard cache older than 24 hours
}}
buster
Version your cache to invalidate old formats:
persistOptions={{
persister,
buster: 'v2', // Changing this invalidates all old caches
}}
Increment the buster value when making breaking changes to your data structure to automatically invalidate old cached data.
Custom Persister
Create a custom persister for any storage:
import { experimental_createPersister } from '@tanstack/query-persist-client-core'
const customPersister = experimental_createPersister({
storage: {
getItem: async (key: string) => {
// Implement your custom get logic
const data = await yourCustomStorage.get(key)
return data
},
setItem: async (key: string, value: string) => {
// Implement your custom set logic
await yourCustomStorage.set(key, value)
},
removeItem: async (key: string) => {
// Implement your custom remove logic
await yourCustomStorage.remove(key)
},
},
})
Selective Persistence
Choose which queries to persist:
persistOptions={{
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// Only persist queries with specific keys
const queryKey = query.queryKey[0]
return ['posts', 'users', 'comments'].includes(queryKey as string)
},
},
}}
Exclude Sensitive Data
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// Don't persist sensitive queries
const queryKey = query.queryKey[0]
const sensitiveKeys = ['user-credentials', 'payment-info', 'auth-token']
return !sensitiveKeys.includes(queryKey as string)
},
}
Only Persist Recent Queries
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
const HOUR = 1000 * 60 * 60
const queryAge = Date.now() - query.state.dataUpdatedAt
return queryAge < HOUR // Only persist queries less than 1 hour old
},
}
Data Serialization
Transform data during persist/restore:
persistOptions={{
persister,
dehydrateOptions: {
serializeData: (data) => {
// Custom serialization (e.g., handle Dates)
return JSON.stringify(data, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() }
}
return value
})
},
},
hydrateOptions: {
deserializeData: (data) => {
// Custom deserialization
return JSON.parse(data, (key, value) => {
if (value?.__type === 'Date') {
return new Date(value.value)
}
return value
})
},
},
}}
Manual Persistence
For more control, use the persistence functions directly:
import {
persistQueryClient,
persistQueryClientSave,
persistQueryClientRestore,
} from '@tanstack/query-persist-client-core'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const queryClient = new QueryClient()
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
// Restore on app start
await persistQueryClientRestore({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24,
})
// Manually save
await persistQueryClientSave({
queryClient,
persister,
})
// Auto-persist on cache changes
const unsubscribe = persistQueryClientSubscribe({
queryClient,
persister,
})
// Clean up
unsubscribe()
Persist Mutations
Persist paused mutations (e.g., offline support):
dehydrateOptions: {
shouldDehydrateMutation: (mutation) => {
// Persist paused mutations (offline mutations)
return mutation.state.isPaused
},
}
Error Handling
Handle persistence errors:
import { persistQueryClient } from '@tanstack/query-persist-client-core'
try {
await persistQueryClientRestore({
queryClient,
persister,
})
} catch (error) {
console.error('Failed to restore cache:', error)
// Fall back to empty cache
}
Storage Quota Management
Handle storage quota exceeded:
const persister = experimental_createPersister({
storage: {
setItem: async (key, value) => {
try {
localStorage.setItem(key, value)
} catch (error) {
if (error.name === 'QuotaExceededError') {
// Clear old data
localStorage.removeItem(key)
// Try again
localStorage.setItem(key, value)
}
}
},
getItem: (key) => localStorage.getItem(key),
removeItem: (key) => localStorage.removeItem(key),
},
})
Compression
Compress persisted data to save space:
import LZString from 'lz-string'
const persister = experimental_createPersister({
storage: {
setItem: async (key, value) => {
const compressed = LZString.compress(value)
localStorage.setItem(key, compressed)
},
getItem: async (key) => {
const compressed = localStorage.getItem(key)
return compressed ? LZString.decompress(compressed) : null
},
removeItem: (key) => localStorage.removeItem(key),
},
})
Multi-Tab Synchronization
Sync cache across browser tabs:
import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental'
const queryClient = new QueryClient()
// Enable cross-tab sync
broadcastQueryClient({
queryClient,
broadcastChannel: 'my-app-cache',
})
Testing with Persistence
import { QueryClient } from '@tanstack/react-query'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { persistQueryClient } from '@tanstack/query-persist-client-core'
test('should persist and restore queries', async () => {
const queryClient = new QueryClient()
const persister = createSyncStoragePersister({
storage: window.localStorage,
key: 'TEST_CACHE',
})
// Add data to cache
queryClient.setQueryData(['test'], { value: 'data' })
// Persist
await persistQueryClientSave({ queryClient, persister })
// Create new client
const newQueryClient = new QueryClient()
// Restore
await persistQueryClientRestore({
queryClient: newQueryClient,
persister,
})
// Verify data restored
expect(newQueryClient.getQueryData(['test'])).toEqual({ value: 'data' })
// Cleanup
localStorage.removeItem('TEST_CACHE')
})
Best Practices
- Set appropriate maxAge - Don’t persist stale data indefinitely
- Use cache busting - Version your cache for breaking changes
- Exclude sensitive data - Never persist auth tokens, passwords, etc.
- Handle errors gracefully - App should work even if restoration fails
- Monitor storage size - Especially important for mobile apps
- Compress large datasets - Use compression for better performance
Common Pitfalls
1. Not Setting gcTime
// ❌ Bad - Queries removed from cache before persistence
const queryClient = new QueryClient() // default gcTime is 5 minutes
// ✅ Good - Keep queries in cache long enough to persist
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
2. Persisting Everything
// ❌ Bad - Persists all queries including sensitive/temporary data
persistOptions={{ persister }}
// ✅ Good - Only persist what you need
persistOptions={{
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// Selective persistence
return query.state.status === 'success' && !query.meta?.sensitive
},
},
}}
3. Forgetting Cache Version
// ❌ Bad - Old cache format breaks new app version
persistOptions={{ persister }}
// ✅ Good - Version your cache
persistOptions={{
persister,
buster: 'v1', // Increment when data structure changes
}}
Next Steps
- SSR - Combine persistence with server-side rendering