Back to Ragflow

persist-queries: Configure Query Persistence for Offline Support

web/.agents/skills/tanstack-query-best-practices/rules/persist-queries.md

0.25.14.8 KB
Original Source

persist-queries: Configure Query Persistence for Offline Support

Priority: LOW

Explanation

TanStack Query can persist the cache to storage (localStorage, IndexedDB, AsyncStorage) and restore it on app load. This enables offline support and faster startup by eliminating initial loading states.

Bad Example

tsx
// No persistence - always starts fresh
const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  )
}

// User refreshes page:
// 1. Empty cache
// 2. Loading spinners everywhere
// 3. Refetch all data
// Poor offline experience

Good Example: Basic Persistence with localStorage

tsx
import { QueryClient } from '@tanstack/react-query'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24,  // 24 hours - keep cache longer for persistence
      staleTime: 1000 * 60 * 5,     // 5 minutes
    },
  },
})

const persister = createSyncStoragePersister({
  storage: window.localStorage,
  key: 'REACT_QUERY_CACHE',
})

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{
        persister,
        maxAge: 1000 * 60 * 60 * 24,  // 24 hours max
      }}
    >
      <MyApp />
    </PersistQueryClientProvider>
  )
}

Good Example: Async Persistence with IndexedDB

tsx
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { get, set, del } from 'idb-keyval'

const persister = createAsyncStoragePersister({
  storage: {
    getItem: async (key) => await get(key),
    setItem: async (key, value) => await set(key, value),
    removeItem: async (key) => await del(key),
  },
  key: 'REACT_QUERY_CACHE',
})

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{
        persister,
        maxAge: 1000 * 60 * 60 * 24 * 7,  // 7 days
        buster: APP_VERSION,  // Bust cache on app updates
      }}
    >
      <MyApp />
    </PersistQueryClientProvider>
  )
}

Good Example: Selective Persistence

tsx
import { persistQueryClient } from '@tanstack/react-query-persist-client'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24,
    },
  },
})

// Only persist certain queries
persistQueryClient({
  queryClient,
  persister,
  dehydrateOptions: {
    shouldDehydrateQuery: (query) => {
      // Don't persist user-specific sensitive data
      if (query.queryKey[0] === 'user-session') return false
      // Don't persist real-time data
      if (query.queryKey[0] === 'notifications') return false
      // Don't persist failed queries
      if (query.state.status !== 'success') return false
      // Persist everything else
      return true
    },
  },
})

Good Example: React Native with AsyncStorage

tsx
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'

const persister = createAsyncStoragePersister({
  storage: AsyncStorage,
  key: 'app-query-cache',
})

// Usage is the same as web

Good Example: Handling Restoration Loading

tsx
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // Cache restored successfully
        console.log('Cache restored')
      }}
    >
      <PersistQueryClientProvider.Consumer>
        {({ isRestoring }) =>
          isRestoring ? <SplashScreen /> : <MainApp />
        }
      </PersistQueryClientProvider.Consumer>
    </PersistQueryClientProvider>
  )
}

// Or use the hook
function MainApp() {
  const { isRestoring } = usePersistQueryClientRestore()

  if (isRestoring) return <SplashScreen />
  return <App />
}

Persistence Configuration

OptionPurpose
maxAgeMaximum cache age before considered invalid
busterString to invalidate cache (use app version)
dehydrateOptions.shouldDehydrateQueryFilter which queries to persist
hydrateOptions.shouldHydrateFilter which queries to restore

Context

  • Requires @tanstack/react-query-persist-client package
  • Set gcTime higher than default (5 min) for persistence to be useful
  • Use buster option to invalidate cache on app updates
  • Don't persist sensitive data or real-time data
  • IndexedDB is better than localStorage for large caches
  • Restored data is still subject to staleTime checks
  • Works well with networkMode: 'offlineFirst'