CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Utilities for persisting TanStack Query cache data to various storage backends with restore functionality

Pending
Overview
Eval results
Files

query-persistence.mddocs/

Fine-Grained Query Persistence

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.

Capabilities

Create Query Persister

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 entries

Storage Configuration

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

AsyncStorage Interface

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

Persisted Query Structure

Structure used for individual persisted queries.

interface PersistedQuery {
  buster: string;
  queryHash: string;
  queryKey: QueryKey;
  state: QueryState;
}

Advanced Usage

Custom Serialization

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

Selective Query Filtering

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

Garbage Collection

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

Selective Restoration

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

Constants

/** Default prefix for storage keys */
const PERSISTER_KEY_PREFIX = 'tanstack-query';

Error Handling

Fine-grained persistence includes comprehensive error handling:

  • Storage Failures: Gracefully handles storage quota exceeded, permission errors, and network failures
  • Serialization Errors: Catches and logs serialization/deserialization failures
  • Expired Data: Automatically removes expired queries during retrieval
  • Development Warnings: Logs helpful warnings in development mode for debugging
  • Storage Feature Detection: Handles missing storage methods gracefully (e.g., when 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

docs

client-persistence.md

index.md

query-persistence.md

retry-strategies.md

tile.json