createSyncStoragePersister example with localStorage
Example showing basic setup:
```tsx
import { persistQueryClient } 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 localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister: localStoragePersister,
})
```
PersistRetryer function signature
The PersistRetryer is a function that receives an object with persistedClient (the PersistedClient that failed to save), error (the Error that occurred), and errorCount (number of retry attempts). It must return a new PersistedClient to retry persistence, or undefined to stop retrying.
removeOldestQuery retry strategy
The removeOldestQuery is a predefined retry strategy that can be imported from '@tanstack/react-query-persist-client'. It returns a new PersistedClient with the oldest query removed, allowing persistence to complete when storage space is exceeded.
createSyncStoragePersister options interface
CreateSyncStoragePersisterOptions interface has the following properties:
- storage: Storage | undefined | null (required) - The storage client used for setting and retrieving items from cache (window.localStorage or window.sessionStorage)
- key?: string (optional) - The key to use when storing the cache, defaults to 'REACT_QUERY_OFFLINE_CACHE'
- throttleTime?: number (optional) - Time in ms to throttle saving the cache to disk, defaults to 1000
- serialize?: (client: PersistedClient) => string (optional) - How to serialize the data to storage, defaults to JSON.stringify
- deserialize?: (cachedString: string) => PersistedClient (optional) - How to deserialize the data from storage, defaults to JSON.parse
- retry?: PersistRetryer (optional) - How to retry persistence on error, defaults to no retry
Using lz-string compression with createSyncStoragePersister
Example using lz-string for compression to store more data in localStorage:
```tsx
import { QueryClient } from '@tanstack/react-query'
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { compress, decompress } from 'lz-string'
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity } },
})
persistQueryClient({
queryClient: queryClient,
persister: createSyncStoragePersister({
storage: window.localStorage,
serialize: (data) => compress(JSON.stringify(data)),
deserialize: (data) => JSON.parse(decompress(data)),
}),
maxAge: Infinity,
})
```
Installation of createSyncStoragePersister
To use createSyncStoragePersister, install the packages '@tanstack/query-sync-storage-persister' and '@tanstack/react-query-persist-client' using npm, pnpm, yarn, or bun.
Basic usage of createSyncStoragePersister
Import createSyncStoragePersister from '@tanstack/query-sync-storage-persister' and pass a storage object (such as window.localStorage or window.sessionStorage) to it. Pass the resulting persister to persistQueryClient along with a QueryClient instance.
createSyncStoragePersister is deprecated
The createSyncStoragePersister plugin is deprecated and will be removed in the next major version. Users should use '@tanstack/query-async-storage-persister' instead.
persistQueryClient combined function
persistQueryClient({queryClient, persister, maxAge = 1000 * 60 * 60 * 24, buster = '', hydrateOptions = undefined, dehydrateOptions = undefined}) performs two actions: it immediately restores any persisted cache (like persistQueryClientRestore), then subscribes to the query cache and returns the unsubscribe function (like persistQueryClientSubscribe). This functionality is preserved from version 3.x.
PersistQueryClientOptions interface
interface PersistQueryClientOptions { queryClient: QueryClient (required, the QueryClient to persist); persister: Persister (required, the Persister interface for storing and restoring cache); maxAge?: number (the max-allowed age of cache in milliseconds, defaults to 24 hours, older caches are silently discarded); buster?: string (unique string to forcefully invalidate caches without matching buster); hydrateOptions?: HydrateOptions (options passed to hydrate function, not used in persistQueryClientSave or persistQueryClientSubscribe); dehydrateOptions?: DehydrateOptions (options passed to dehydrate function, not used in persistQueryClientRestore) }
PersistQueryClientProvider for React
PersistQueryClientProvider is a React component that ensures proper subscription/unsubscription according to React component lifecycle and prevents queries from fetching during cache restoration. Queries will render but be in fetchingState: 'idle' until restoration is complete, then refetch unless the restored data is fresh enough. It respects initialData and can be used instead of QueryClientProvider.
persistQueryClient overview and purpose
persistQueryClient is a set of utilities for interacting with persisters that save the queryClient for later use. Different persisters can be used to store the client and cache to many different storage layers. Available persisters include createSyncStoragePersister, createAsyncStoragePersister, or custom persisters.
gcTime configuration for persistence
When using persistQueryClient, the QueryClient should be created with a gcTime value to override the default during hydration. If not set, gcTime defaults to 300000 (5 minutes) for hydration and the stored cache will be discarded after 5 minutes of inactivity. The gcTime should be set to the same value or higher than persistQueryClient's maxAge option. For example, if maxAge is 24 hours (the default), gcTime should be 24 hours or higher. If gcTime is lower than maxAge, garbage collection will discard the stored cache earlier than expected. You can pass Infinity to disable garbage collection entirely. The maximum allowed gcTime is about 24 days due to JavaScript limitations.
Cache busting with buster option
The buster option is a unique string used to forcefully invalidate existing caches if they do not share the same buster string. If a persisted cache is found that does not have the matching buster string, it will be discarded. The persistQueryClient, persistQueryClientSave, and persistQueryClientRestore functions all accept this option.
Cache removal conditions
The persister's removeClient() method is called and the cache is immediately discarded if any of the following conditions are met: the cache is expired (see maxAge), the cache is busted (see buster), an error occurs (ex: throws), or the cache is empty (ex: undefined).
persistQueryClientSave API
persistQueryClientSave({queryClient, persister, buster = '', dehydrateOptions = undefined}) dehydrates query/mutation data and stores it via the provided persister. The createSyncStoragePersister and createAsyncStoragePersister throttle this to happen at most every 1 second to reduce expensive writes. This function can be used to explicitly persist the cache at chosen moments.
persistQueryClientSubscribe API
persistQueryClientSubscribe({queryClient, persister, buster = '', dehydrateOptions = undefined}) runs persistQueryClientSave whenever the cache changes. It returns an unsubscribe function to discontinue monitoring and stop updates to the persisted cache. To erase the persisted cache after unsubscribing, pass a new buster to persistQueryClientRestore to trigger the persister's removeClient function.
persistQueryClientRestore API
persistQueryClientRestore({queryClient, persister, maxAge = 1000 * 60 * 60 * 24, buster = '', hydrateOptions = undefined}) attempts to hydrate a previously persisted dehydrated query/mutation cache from the persister back into the query cache. If a cache is found that is older than maxAge (24 hours by default), it will be discarded. This can be used to restore the cache at chosen moments.
PersistQueryClientProvider props
PersistQueryClientProvider takes all props of QueryClientProvider plus: persistOptions: PersistQueryClientOptions (all options from persistQueryClient minus the QueryClient itself); onSuccess?: () => Promise<unknown> | unknown (optional, called when initial restore finishes, can be used to resumePausedMutations, awaited if Promise is returned); onError?: () => Promise<unknown> | unknown (optional, called when error is thrown during restoration, awaited if Promise is returned).
useIsRestoring hook
useIsRestoring is a hook available when using PersistQueryClientProvider that checks if a restore is currently in progress. useQuery and other query hooks also check this internally to avoid race conditions between restore and mounting queries.
Persister interface
export interface Persister { persistClient(persistClient: PersistedClient): Promisable<void>; restoreClient(): Promisable<PersistedClient | undefined>; removeClient(): Promisable<void>; }
PersistedClient interface
export interface PersistedClient { timestamp: number; buster: string; clientState: DehydratedState; }
Creating an IndexedDB persister example
import { get, set, del } from 'idb-keyval'; import { PersistedClient, Persister } from '@tanstack/react-query-persist-client'; export function createIDBPersister(idbValidKey: IDBValidKey = 'reactQuery') { return { persistClient: async (client: PersistedClient) => { await set(idbValidKey, client) }, restoreClient: async () => { return await get<PersistedClient>(idbValidKey) }, removeClient: async () => { await del(idbValidKey) }, } satisfies Persister }. IndexedDB is faster than Web Storage API, stores more than 5MB, doesn't require serialization, and can store JavaScript native types like Date and File.
PersistQueryClientProvider usage example
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 } } }); const persister = createAsyncStoragePersister({ storage: window.localStorage }); ReactDOM.createRoot(rootElement).render(<PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}><App /></PersistQueryClientProvider>);
Race condition issues with persistQueryClient
When using persistQueryClient directly without PersistQueryClientProvider, restoring is asynchronous and can cause race conditions if your App renders while restoring. If a query mounts and fetches at the same time as restoration is happening, race conditions can occur. Additionally, if you subscribe to changes outside of React component lifecycle, there is no way to unsubscribe.
HydrationBoundary API reference
HydrationBoundary component parameters: state (required, DehydratedState) - the state to hydrate; options (optional, HydrateOptions) - defaultOptions (optional, QueryOptions) - the default query options to use for the hydrated queries; queryClient (optional, QueryClient) - use custom QueryClient, otherwise one from nearest context will be used. Usage: <HydrationBoundary state={dehydratedState}>...</HydrationBoundary>
dehydrate function creates frozen cache representation
The dehydrate function creates a frozen representation of a cache that can later be hydrated with HydrationBoundary or hydrate. This is useful for passing prefetched queries from server to client or persisting queries to localStorage or other persistent locations. It only includes currently successful queries by default.
hydrate function adds dehydrated state to cache
The hydrate function adds a previously dehydrated state into a cache.
dehydrate API reference
dehydrate(queryClient, options) parameters: client (required, QueryClient) - the queryClient that should be dehydrated; options (optional, DehydrateOptions) - shouldDehydrateMutation (optional, function) - whether to dehydrate mutations, defaults to only including paused mutations, function called for each mutation returning true to include or false otherwise, can import defaultShouldDehydrateMutation to extend default behavior; shouldDehydrateQuery (optional, function) - whether to dehydrate queries, defaults to only including successful queries, function called for each query returning true to include or false otherwise, can import defaultShouldDehydrateQuery to extend default behavior; serializeData (optional, function) - transforms data during dehydration; shouldRedactErrors (optional, function) - whether to redact errors from server, defaults to redacting all errors, function called for each error returning true to redact or false otherwise. Returns DehydratedState containing everything needed to hydrate the queryClient later. The exact format is not part of the public API and can change. Result is not in serialized form, serialization must be done manually if desired.
dehydrate limitation with non-JSON serializable values
Some storage systems like browser Web Storage API require values to be JSON serializable. If dehydrating values that are not automatically JSON serializable like Error or undefined, they must be manually serialized. Since only successful queries are included by default, to also include Errors, provide shouldDehydrateQuery option and serialize Error instances to objects before storing, then deserialize objects back to Error instances when hydrating.
dehydrate with Error objects example
Example showing how to handle non-JSON serializable values when dehydrating: on server, call dehydrate(client, { shouldDehydrateQuery: () => true }) to include Errors, then transform with mySerialize(state) to transform Error instances to objects; on client, call myDeserialize(serializedState) to transform objects back to Error instances, then hydrate(client, state).
hydrate API reference
hydrate(queryClient, dehydratedState, options) parameters: client (required, QueryClient) - the queryClient to hydrate the state into; dehydratedState (required, DehydratedState) - the state to hydrate into the client; options (optional, HydrateOptions) - defaultOptions (optional, DefaultOptions) with mutations (optional, MutationOptions) for default mutation options and queries (optional, QueryOptions) for default query options, deserializeData (optional, function) - transforms data before it is put into the cache; queryClient (optional, QueryClient) - use custom QueryClient, otherwise one from nearest context will be used.
hydrate limitation with existing queries in cache
If queries being hydrated already exist in the queryCache, hydrate will only overwrite them if the data is newer than the data present in the cache. Otherwise, the new data will not be applied.
HydrationBoundary component adds dehydrated state to queryClient
HydrationBoundary adds a previously dehydrated state into the queryClient that would be returned by useQueryClient(). If the client already contains data, the new queries will be intelligently merged based on update timestamp. Only queries can be dehydrated with HydrationBoundary, not mutations.