Skip to main content

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

  1. Set appropriate maxAge - Don’t persist stale data indefinitely
  2. Use cache busting - Version your cache for breaking changes
  3. Exclude sensitive data - Never persist auth tokens, passwords, etc.
  4. Handle errors gracefully - App should work even if restoration fails
  5. Monitor storage size - Especially important for mobile apps
  6. 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

Build docs developers (and LLMs) love