Capítulo 45 de 80

Chapter 45: Migrating to TanStack Query v5

Core Idea

v5's breaking changes cluster around one theme — a single object-argument signature everywhere (no more overloads) — plus several renames that fix long-standing footguns (cacheTimegcTime, status: 'loading''pending'), and query-level success/error/settled callbacks were removed entirely in favor of useEffect/derived state.

Key Concepts

  • Single object signature everywhere: every hook and queryClient/queryCache method that used to accept (key, fn, options) or (key, filters) now takes one object — useQuery({ queryKey, queryFn, ...options }), queryClient.invalidateQueries({ queryKey, ...filters }), etc. A codemod (jscodeshift + the shipped transform) automates most of this migration.
  • Imperative methods consolidated: fetchQuery/prefetchQuery/ensureQueryData (and their infinite-query equivalents) are deprecated in favor of queryClient.query()/queryClient.infiniteQuery() — prefetch by discarding the promise with .catch(noop), and "ensure" behavior by passing staleTime: 'static'.
  • Renames: cacheTimegcTime (the old name misleadingly implied "how long data is cached," when it actually means "how long unused data survives before GC"); useErrorBoundarythrowOnError; hashQueryKeyhashKey; status: 'loading'/isLoadingstatus: 'pending'/isPending, with a new isLoading now meaning isPending && isFetching (what isInitialLoading, now deprecated, used to mean); Hydrate component → HydrationBoundary.
  • Query-level callbacks removed: onSuccess/onError/onSettled no longer exist on useQuery/QueryObserver (mutations keep theirs) — replace with a useEffect reacting to data/error, or derive UI directly from query state.
  • keepPreviousData folded into placeholderData: pass the built-in keepPreviousData identity function (or your own (previousData) => previousData) as placeholderData instead of the removed keepPreviousData: true option; isPreviousData is now isPlaceholderData. Caveat: unlike the old option, this always reports success status (never propagates the previous query's error status) and dataUpdatedAt resets to 0 rather than carrying over.
  • Infinite queries now require initialPageParam: pageParam is no longer silently undefined-defaulted inside queryFn (that value wasn't serializable) — pass initialPageParam explicitly on the query options instead. Manual pageParam overrides to fetchNextPage/fetchPreviousPage ("manual mode") were removed, making getNextPageParam mandatory. null now also signals "no more pages," not just undefined.
  • dehydrate simplified: boolean dehydrateQueries/dehydrateMutations options are gone — use function equivalents shouldDehydrateQuery/shouldDehydrateMutation (pass () => false to restore "dehydrate nothing").
  • Connectivity detection hardened: window-focus refetching now uses only visibilitychange (not the focus event); online/offline detection now starts optimistically online: true and only updates via online/offline events, instead of trusting the unreliable navigator.onLine property.
  • Context/microfrontend isolation: the custom context prop is gone — pass a custom queryClient instance directly to each hook instead for the same multi-instance isolation (this also removed QueryClientProvider's contextSharing prop).
  • refetchPage removed in favor of maxPages: the old per-page refetch override is replaced by capping how many pages an infinite query keeps/refetches via maxPages (requires both getNextPageParam/getPreviousPageParam for bi-directional support).
  • Requires React 18+: due to internal use of useSyncExternalStore; unstable_batchedUpdates batching was also dropped (a no-op in React 18) — frameworks needing custom batching call notifyManager.setBatchNotifyFunction.
  • queryClient.setQueryDefaults ordering flipped: getQueryDefaults now merges all matching registrations instead of returning only the first match — register from most-generic key to least-generic, since later/more-specific registrations now layer on top instead of short-circuiting.
  • New in v5: useSuspenseQuery/useSuspenseInfiniteQuery/useSuspenseQueries (stable, data never undefined at the type level, replacing the experimental suspense: boolean flag); combine option for useQueries; multi-page infinite-query prefetching; the experimental fine-grained experimental_createQueryPersister.

Reference Tables

v4 (or earlier)v5
cacheTimegcTime
useErrorBoundarythrowOnError
status: 'loading' / isLoadingstatus: 'pending' / isPending (new isLoading = isPending && isFetching)
hashQueryKeyhashKey
<Hydrate><HydrationBoundary>
keepPreviousData: trueplaceholderData: keepPreviousData
fetchQuery/prefetchQuery/ensureQueryDataqueryClient.query() (with .catch(noop) or staleTime: 'static')
useQuery(key, fn, options)useQuery({ queryKey, queryFn, ...options })

Key Takeaways

  1. Run the official codemod first for the object-signature migration — it handles the common cases automatically, but review its output (formatting and edge cases need manual cleanup).
  2. Query-level onSuccess/onError/onSettled are gone for good — plan to move that logic into a useEffect or the mutation layer before upgrading, not as an afterthought.
  3. Re-check any code relying on keepPreviousData's old error-status propagation or dataUpdatedAt carry-over — placeholderData's replacement behavior is subtly different, not a drop-in rename.

Connects To

  • Suspense: the newly-stabilized suspense hooks introduced in v5.
  • Query Retries / Important Defaults: gcTime is the renamed option covered in depth there.
  • Infinite Queries: initialPageParam/maxPages, both v5 requirements covered here.