Capítulo 51 de 80

Chapter 51: experimental_createQueryPersister

Core Idea

Unlike persistQueryClient (persists the entire client as one blob), experimental_createQueryPersister persists queries individually — attach it as a query's persister option (globally via defaultOptions or per-useQuery) to choose exactly what's worth persisting, lazily restoring each query only when it's actually used.

Key Concepts

  • Per-query, not whole-client: assign persisterFn to queryClient's defaultOptions.queries.persister (persists everything, narrowable with filters) or to a single useQuery call (persists just that query) — each query is stored under its own key (prefix-queryHash), not bundled into one blob.
  • Lazy restore, eager persist: a query restores from storage the first time it's used, and persists after every queryFn run — no throttling needed since writes are naturally spaced by actual fetches, unlike persistQueryClientSave's explicit 1s throttle.
  • staleTime still governs behavior after restore: fresh restored data doesn't refetch; stale restored data refetches immediately — same rule as any other cache hit.
  • GC-independent: garbage-collecting a query from memory (gcTime expiring) doesn't touch the persisted copy — this decouples memory efficiency from durability, letting gcTime stay short while data still survives across sessions via storage.
  • queryClient.setQueryData isn't persisted: an optimistic update written directly via setQueryData is not automatically saved — a page reload before the query naturally re-runs/invalidates loses that optimistic change unless persisted manually.
  • networkMode defaults to 'offlineFirst' when a persister is attached: since the persister sits between the query and the network as a caching layer, it needs to be able to restore even without connectivity.
  • Extra utilities returned alongside persisterFn: persistQueryByKey(key, queryClient) (manually persist a specific query right away — e.g. right after an optimistic setQueryData, to cover the gap described above); retrieveQuery(queryHash) (fetch a persisted query directly, auto-removing it if expired/busted/malformed); persisterGc() (sweep storage for expired/busted/malformed entries — needs the storage to expose entries()); restoreQueries(queryClient, filters) (bulk-restore matching queries up front, e.g. at app startup for instant offline-available data); removeQueries(filters) (persisted data isn't auto-removed by queryClient.removeQueries — call this too if it must actually be deleted from storage).
  • Key options: storage (AsyncStorage-shaped, or Storage, or undefined/null for SSR); maxAge (default 24h); buster; prefix (default 'tanstack-query', forms the storage key with the query hash); refetchOnRestore (true default — refetch if stale after restore; false — never; 'always' — unconditionally); filters (a QueryFilters object narrowing which queries this persister applies to).

Code Examples

const persister = experimental_createQueryPersister({
  storage: AsyncStorage,
  maxAge: 1000 * 60 * 60 * 12,
})

const queryClient = new QueryClient({
  defaultOptions: { queries: { gcTime: 1000 * 30, persister: persister.persisterFn } },
})

// Covering the setQueryData gap: persist an optimistic update immediately
useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    persister.persistQueryByKey(['todos'], queryClient)
  },
})
  • What it demonstrates: a short in-memory gcTime paired with a much longer persisted maxAge, and manually persisting an optimistic update that setQueryData alone wouldn't save.

Key Takeaways

  1. Reach for this over persistQueryClient when only some queries are worth persisting, or when per-query granularity (different maxAge, different storage) matters — it's the selective alternative.
  2. Always call persistQueryByKey after an optimistic setQueryData write if that change must survive a reload before the mutation settles — it isn't covered automatically.
  3. Remember queryClient.removeQueries doesn't clean up persisted storage — pair it with this persister's own removeQueries when data must actually be deleted, not just evicted from memory.

Connects To

  • persistQueryClient: the whole-client alternative to this per-query approach.
  • Network Mode: the offlineFirst default this persister activates.
  • Optimistic Updates: the setQueryData gap this persister's persistQueryByKey addresses.