CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--react-query-persist-client

React bindings to work with persisters in TanStack/react-query

Pending
Overview
Eval results
Files

experimental-features.mddocs/

Experimental Features

The package includes experimental functionality for fine-grained, per-query persistence control. These features are marked as experimental and may change in future versions.

Capabilities

experimental_createQueryPersister Function

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>;
}

AsyncStorage Interface

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>;

Retry Strategies

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;

Advanced Usage Examples

Custom Storage Implementation

// 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',
});

Selective Query Persistence

// 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);
  },
});

Manual Garbage Collection

// 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 hour

Batch Query Restoration

import { 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
}

Custom Query Function with Persistence

// 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>;
}

Constants

/** 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

docs

core-persistence.md

experimental-features.md

index.md

persister-interface.md

react-provider.md

tile.json