Utilities for persisting TanStack Query cache data to various storage backends with restore functionality
—
Warning: Experimental feature - The fine-grained query persistence API is experimental and may change in future versions.
This capability enables query-by-query persistence with selective restoration and advanced storage management. Unlike client-level persistence which saves entire cache state, this approach allows granular control over which queries are persisted and restored.
Factory function that creates a persister object with methods for managing individual query persistence.
/**
* Creates fine-grained query persistence functionality (experimental feature)
* @param options - Configuration options for the persister
* @returns Persister object with query management methods
*/
function experimental_createQueryPersister<TStorageValue = string>(
options: StoragePersisterOptions<TStorageValue>
): QueryPersisterObject;
interface QueryPersisterObject {
persisterFn: <T, TQueryKey extends QueryKey>(
queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
ctx: QueryFunctionContext<TQueryKey>,
query: Query
) => Promise<T>;
persistQuery: (query: Query) => Promise<void>;
persistQueryByKey: (queryKey: QueryKey, queryClient: QueryClient) => Promise<void>;
retrieveQuery: <T>(
queryHash: string,
afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void
) => Promise<T | undefined>;
persisterGc: () => Promise<void>;
restoreQueries: (
queryClient: QueryClient,
filters?: Pick<QueryFilters, 'queryKey' | 'exact'>
) => Promise<void>;
}Usage Example:
import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core";
import { useQuery } from "@tanstack/react-query";
// Create persister with localStorage
const persister = experimental_createQueryPersister({
storage: localStorage,
buster: "app-v1.0",
maxAge: 1000 * 60 * 60 * 6, // 6 hours
prefix: "my-app-cache",
filters: {
predicate: (query) => {
// Only persist successful queries with specific keys
return query.state.status === 'success' &&
query.queryKey[0] === 'user-data';
}
}
});
// Use with individual queries
function UserProfile({ userId }) {
return useQuery({
queryKey: ['user-data', userId],
queryFn: ({ queryKey }) => fetchUser(queryKey[1]),
persister: persister.persisterFn,
});
}
// Manual query operations
await persister.persistQueryByKey(['user-data', '123'], queryClient);
await persister.restoreQueries(queryClient, { queryKey: ['user-data'] });
await persister.persisterGc(); // Clean up expired entriesConfigure how data is stored and retrieved from the storage backend.
interface StoragePersisterOptions<TStorageValue = string> {
/** The storage client used for setting and retrieving items from cache.
* For SSR pass in `undefined`. */
storage: AsyncStorage<TStorageValue> | undefined | null;
/**
* How to serialize the data to storage.
* @default `JSON.stringify`
*/
serialize?: (persistedQuery: PersistedQuery) => MaybePromise<TStorageValue>;
/**
* How to deserialize the data from storage.
* @default `JSON.parse`
*/
deserialize?: (cachedString: TStorageValue) => MaybePromise<PersistedQuery>;
/**
* A unique string that can be used to forcefully invalidate existing caches,
* if they do not share the same buster string
*/
buster?: string;
/**
* The max-allowed age of the cache in milliseconds.
* If a persisted cache is found that is older than this
* time, it will be discarded
* @default 24 hours
*/
maxAge?: number;
/**
* Prefix to be used for storage key.
* Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`.
* @default 'tanstack-query'
*/
prefix?: string;
/**
* Filters to narrow down which Queries should be persisted.
*/
filters?: QueryFilters;
}Generic interface for storage implementations that can be synchronous or asynchronous.
interface AsyncStorage<TStorageValue = string> {
getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>;
setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>;
removeItem: (key: string) => MaybePromise<void>;
/** Optional method for iterating over all stored entries (required for garbage collection and restore) */
entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>;
}
type MaybePromise<T> = T | Promise<T>;Storage Implementation Examples:
// Browser localStorage implementation
const localStorageAdapter: AsyncStorage<string> = {
getItem: (key) => localStorage.getItem(key),
setItem: (key, value) => localStorage.setItem(key, value),
removeItem: (key) => localStorage.removeItem(key),
entries: () => {
const entries: Array<[string, string]> = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
const value = localStorage.getItem(key);
if (value) entries.push([key, value]);
}
}
return entries;
}
};
// React Native AsyncStorage implementation
import AsyncStorageRN from '@react-native-async-storage/async-storage';
const asyncStorageAdapter: AsyncStorage<string> = {
getItem: AsyncStorageRN.getItem,
setItem: AsyncStorageRN.setItem,
removeItem: AsyncStorageRN.removeItem,
entries: async () => {
const keys = await AsyncStorageRN.getAllKeys();
const items = await AsyncStorageRN.multiGet(keys);
return items.filter(([, value]) => value !== null) as Array<[string, string]>;
}
};
// Custom binary storage implementation
const binaryStorageAdapter: AsyncStorage<Uint8Array> = {
getItem: async (key) => {
const data = await customBinaryStore.get(key);
return data || null;
},
setItem: async (key, value) => {
await customBinaryStore.set(key, value);
},
removeItem: async (key) => {
await customBinaryStore.delete(key);
},
entries: async () => {
return await customBinaryStore.getAllEntries();
}
};Structure used for individual persisted queries.
interface PersistedQuery {
buster: string;
queryHash: string;
queryKey: QueryKey;
state: QueryState;
}import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core";
// Custom compression for large data
const persister = experimental_createQueryPersister({
storage: localStorage,
serialize: async (query) => {
const json = JSON.stringify(query);
return await compress(json); // Custom compression
},
deserialize: async (compressed) => {
const json = await decompress(compressed);
return JSON.parse(json);
}
});const persister = experimental_createQueryPersister({
storage: localStorage,
filters: {
predicate: (query) => {
// Only persist user and settings queries
const [queryType] = query.queryKey;
return ['user', 'settings'].includes(queryType as string);
}
}
});// Clean up expired entries periodically
setInterval(async () => {
try {
await persister.persisterGc();
console.log('Cache garbage collection completed');
} catch (error) {
console.error('Garbage collection failed:', error);
}
}, 1000 * 60 * 60); // Every hour// Restore only specific queries
await persister.restoreQueries(queryClient, {
queryKey: ['user-data'],
exact: false // Partial match - restores 'user-data.*'
});
// Restore exact query
await persister.restoreQueries(queryClient, {
queryKey: ['user-data', '123'],
exact: true
});/** Default prefix for storage keys */
const PERSISTER_KEY_PREFIX = 'tanstack-query';Fine-grained persistence includes comprehensive error handling:
entries is not available)All operations are designed to fail gracefully without affecting query execution.
Install with Tessl CLI
npx tessl i tessl/npm-tanstack--query-persist-client-core