React bindings to work with persisters in TanStack/react-query
—
The package includes experimental functionality for fine-grained, per-query persistence control. These features are marked as experimental and may change in future versions.
Creates a fine-grained query persister that enables per-query persistence control and storage management.
/**
* Warning: experimental feature.
* Creates a fine-grained query persister for per-query persistence control
* Enables individual queries to be persisted to storage with custom configuration
* @param options - Storage and configuration options
* @returns Object with persister functions for query-level operations
*/
function experimental_createQueryPersister<TStorageValue = string>(
options: StoragePersisterOptions<TStorageValue>
): QueryPersister;
interface StoragePersisterOptions<TStorageValue = string> {
/** The storage client used for setting and retrieving items from cache */
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>;
/** Cache invalidation string for version control */
buster?: string;
/** Max age in milliseconds (default: 24 hours) */
maxAge?: number;
/** Storage key prefix (default: 'tanstack-query') */
prefix?: string;
/** Query filters to narrow down which queries should be persisted */
filters?: QueryFilters;
}
interface QueryPersister {
/** Custom query function that handles persistence and fetching */
persisterFn: <T, TQueryKey extends QueryKey>(
queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
ctx: QueryFunctionContext<TQueryKey>,
query: Query
) => Promise<T>;
/** Persist a specific query to storage */
persistQuery: (query: Query) => Promise<void>;
/** Persist a query by its key */
persistQueryByKey: (queryKey: QueryKey, queryClient: QueryClient) => Promise<void>;
/** Retrieve a query from storage */
retrieveQuery: <T>(
queryHash: string,
afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void
) => Promise<T | undefined>;
/** Garbage collect expired queries from storage */
persisterGc: () => Promise<void>;
/** Restore multiple queries from storage */
restoreQueries: (
queryClient: QueryClient,
filters?: Pick<QueryFilters, 'queryKey' | 'exact'>
) => Promise<void>;
}Usage Example:
import { experimental_createQueryPersister } from '@tanstack/react-query-persist-client';
import { useQuery } from '@tanstack/react-query';
// Create the persister
const queryPersister = experimental_createQueryPersister({
storage: localStorage,
prefix: 'my-app-queries',
maxAge: 1000 * 60 * 60 * 2, // 2 hours
filters: {
// Only persist specific query types
queryKey: ['user-data'],
},
});
// Use with individual queries
function UserProfile({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['user-data', userId],
persister: queryPersister.persisterFn,
queryFn: async ({ queryKey }) => {
const response = await fetch(`/api/users/${queryKey[1]}`);
return response.json();
},
});
return <div>{data?.name}</div>;
}Interface for storage backends used by the experimental persister.
interface AsyncStorage<TStorageValue = string> {
/** Get an item from storage by key */
getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>;
/** Set an item in storage */
setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>;
/** Remove an item from storage */
removeItem: (key: string) => MaybePromise<void>;
/** Get all entries from storage (optional, needed for garbage collection) */
entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>;
}
interface PersistedQuery {
/** Cache invalidation string */
buster: string;
/** Hash of the query key */
queryHash: string;
/** The original query key */
queryKey: QueryKey;
/** The query state including data and metadata */
state: QueryState;
}
type MaybePromise<T> = T | Promise<T>;Built-in retry strategies for handling persistence failures.
/**
* Function type for handling persistence retry scenarios
* Called when persistence fails to determine recovery strategy
*/
type PersistRetryer = (props: {
persistedClient: PersistedClient;
error: Error;
errorCount: number;
}) => PersistedClient | undefined;
/**
* Built-in retry strategy that removes the oldest query when persistence fails
* Useful for storage quota limitations
*/
function removeOldestQuery(props: {
persistedClient: PersistedClient;
error: Error;
errorCount: number;
}): PersistedClient | undefined;// Create a custom storage that combines localStorage with compression
class CompressedStorage implements AsyncStorage<string> {
async getItem(key: string) {
const compressed = localStorage.getItem(key);
return compressed ? LZString.decompress(compressed) : null;
}
async setItem(key: string, value: string) {
const compressed = LZString.compress(value);
localStorage.setItem(key, compressed);
}
async removeItem(key: string) {
localStorage.removeItem(key);
}
async entries() {
const entries: Array<[string, string]> = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
const value = await this.getItem(key);
if (value) entries.push([key, value]);
}
}
return entries;
}
}
const persister = experimental_createQueryPersister({
storage: new CompressedStorage(),
prefix: 'compressed-queries',
});// Only persist queries that match specific criteria
const selectivePersister = experimental_createQueryPersister({
storage: localStorage,
filters: {
// Only persist user-specific data
predicate: (query) => {
const [scope] = query.queryKey as [string, ...any[]];
return ['user-profile', 'user-settings', 'user-preferences'].includes(scope);
},
},
// Custom serializer that excludes sensitive data
serialize: (persistedQuery) => {
const sanitized = {
...persistedQuery,
state: {
...persistedQuery.state,
data: sanitizeUserData(persistedQuery.state.data),
},
};
return JSON.stringify(sanitized);
},
});// Set up periodic garbage collection
const persister = experimental_createQueryPersister({
storage: localStorage,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
// Run garbage collection every hour
setInterval(async () => {
try {
await persister.persisterGc();
console.log('Query cache garbage collection completed');
} catch (error) {
console.error('Garbage collection failed:', error);
}
}, 1000 * 60 * 60); // 1 hourimport { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
const persister = experimental_createQueryPersister({
storage: localStorage,
});
// Restore all cached user data on app startup
async function restoreUserQueries(userId: string) {
await persister.restoreQueries(queryClient, {
queryKey: ['user-data', userId],
exact: false, // Match all queries starting with this key
});
}
// Usage in app initialization
async function initializeApp(userId: string) {
await restoreUserQueries(userId);
// Now user-specific queries are restored from cache
}// Create a custom query function that handles persistence
function createPersistedQueryFn<T>(
baseFetchFn: () => Promise<T>,
persister: ReturnType<typeof experimental_createQueryPersister>
) {
return async (context: QueryFunctionContext) => {
// Let persister handle restoration and caching
return persister.persisterFn(
() => baseFetchFn(),
context,
context.query // Query instance from context
);
};
}
// Usage
const fetchUserData = createPersistedQueryFn(
() => fetch('/api/user').then(r => r.json()),
persister
);
function UserComponent() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUserData,
});
return <div>{data?.name}</div>;
}/** Default prefix used for storage keys */
const PERSISTER_KEY_PREFIX = 'tanstack-query';Storage keys are formed as ${prefix}-${queryHash} where prefix defaults to PERSISTER_KEY_PREFIX.
Install with Tessl CLI
npx tessl i tessl/npm-tanstack--react-query-persist-client