Back to the blog
Reactยท Jun 03, 2026ยท 7 min read

TanStack Query v5 in practice: the new patterns worth adopting

The unified query/mutation API, first-class Suspense, and how the new staleTime: Infinity idiom changes the way we think about server state.

#react#tanstack#async#state-management

TanStack Query v5 is a bigger conceptual shift than the version bump suggests. The surface API looks similar - `useQuery`, `useMutation`, `useQueryClient` - but several defaults changed, the TypeScript types tightened considerably, and the Suspense integration went from 'experimental' to the recommended path for most data fetching. After migrating three production apps, here's what actually changed in practice.

The unified API

The biggest ergonomic win is that `useQuery` and `useMutation` now accept a single options object instead of positional arguments. This sounds like a small detail but it eliminates a whole category of bugs where the wrong argument ended up in the wrong position during refactors. It also makes the API composable - you can define query options once and spread them wherever you use the query.

// v4 (positional args - easy to misplace queryKey vs queryFn)
const { data } = useQuery(['user', userId], () => fetchUser(userId));

// v5 (object - explicit, composable, spreadable)
const userQueryOptions = (userId: string) =>
  queryOptions({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 60_000,
  });

const { data } = useQuery(userQueryOptions(userId));

Suspense as the default

v5 ships `useSuspenseQuery` alongside `useQuery`. The difference: `useSuspenseQuery` always throws a Promise when data is loading (suspending the component), so `data` is always defined in the render path. No more `if (!data) return null` guards. In practice, this makes component code significantly cleaner, but it requires Suspense boundaries higher up the tree - a deliberate architectural trade-off.

staleTime: Infinity and the static data pattern

The new idiom for truly static data (enums, config, reference tables) is `staleTime: Infinity`. This tells Query to never consider the cached data stale, so it won't refetch in the background. For data that only changes on deploys, this is strictly better than a short staleTime - you get the cache hit without the background refetch noise.

  • Default staleTime is now 0ms (same as v4) - data is considered stale immediately after fetching but isn't refetched until it's needed again.
  • gcTime (formerly cacheTime) was renamed to clarify that it controls garbage collection, not when data goes stale.
  • The onSuccess/onError callbacks on useQuery were removed - move side effects into the queryFn or handle them at the useMutation level.
  • Infinite queries now require an initialPageParam instead of inferring it from queryFn args.

Was the migration painful?

The team published a codemods package that handles the positional-to-object argument change automatically. The onSuccess/onError removal is the one change that requires manual work - but it's also the right call. Putting side effects in query callbacks was a footgun that caused subtle bugs when queries ran in the background. Moving effects to explicit event handlers makes data flow clearer.

Written by Appesto Engineering.