Capítulo 47 de 80

Chapter 47: persistQueryClient

Core Idea

persistQueryClient dehydrates the whole QueryClient to a pluggable Persister (storage adapter) and rehydrates it on load — but gcTime must be raised to match or exceed maxAge, or the default 5-minute GC silently discards the persisted cache before it's ever used.

Key Concepts

  • The gcTime/maxAge trap: gcTime defaults to 5 minutes; persistQueryClient's maxAge defaults to 24 hours. If gcTime isn't raised to match, garbage collection runs long before the persisted cache's maxAge window expires, discarding data that should still be valid. Set gcTime to Infinity to disable GC entirely, or match it to maxAge — JS's setTimeout caps effective delays around 24 days, workaroundable via timeoutManager.setTimeoutProvider.
  • Cache busting: pass a buster string (e.g. a build hash) to persistQueryClient/Save/Restore — a persisted cache stored under a different buster is discarded on restore, a clean way to invalidate everything after an incompatible app change.
  • Automatic removal: the persister's removeClient() fires (discarding the cache) if the stored data is expired (maxAge), busted, errored, or empty.
  • Three composable functions: persistQueryClientSave (dehydrate + store now — sync/async persisters throttle this to at most once/second by default); persistQueryClientSubscribe (auto-save on every cache change, returns an unsubscribe); persistQueryClientRestore (hydrate from storage, discarding anything older than maxAge). persistQueryClient itself is the convenience wrapper: restore immediately, then subscribe.
  • React integration — PersistQueryClientProvider: replaces plain QueryClientProvider; correctly subscribes/unsubscribes with the component lifecycle (a bare persistQueryClient() call outside React never unsubscribes) and holds queries in fetchingState: 'idle' until restoration finishes, avoiding a race between mounting queries and an async restore. onSuccess/onError callbacks fire after restore completes/fails — onSuccess is the place to call resumePausedMutations().
  • useIsRestoring: reports whether a restore triggered by PersistQueryClientProvider is still in progress — useQuery and friends check this internally too, to avoid racing a fetch against an in-flight restore.
  • The Persister interface: any storage backend implements just three methods — persistClient, restoreClient, removeClient — operating on a { timestamp, buster, clientState } shape, making custom persisters (e.g. IndexedDB via idb-keyval, chosen for higher storage limits and no serialization requirement) straightforward to build.

Code Examples

const queryClient = new QueryClient({
  defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24 } }, // must be >= maxAge
})

const persister = createAsyncStoragePersister({ storage: window.localStorage })

<PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
  <App />
</PersistQueryClientProvider>
  • What it demonstrates: raising gcTime to match the default maxAge, and wiring persistence through the React-safe provider.

Key Takeaways

  1. Always raise gcTime to at least maxAge (or Infinity) whenever persistence is enabled — this is the single most common way persisted caches silently "don't work."
  2. Prefer PersistQueryClientProvider over calling persistQueryClient() directly outside React — the raw function has no lifecycle-tied unsubscribe and races against initial render.
  3. Building a custom Persister is a three-method interface — reach for it (e.g. IndexedDB) when localStorage's size limits or serialization requirements are a problem.

Connects To

  • createSyncStoragePersister / createAsyncStoragePersister: the built-in persister implementations.
  • QueryClient: gcTime and resumePausedMutations, both central here.
  • Mutations: paused-mutation persistence, resumed via PersistQueryClientProvider's onSuccess.