Skip to main content
v5 is a major version with breaking changes. This guide will help you migrate your application smoothly.

Breaking Changes

Single Object Signature Required

TanStack Query v5 only supports the object format. All previous overloads have been removed.
useQuery(key, fn, options)
useQuery({ queryKey, queryFn, ...options })

useMutation(fn, options)

queryClient.fetchQuery(key, fn, options)

Codemod Available

Run the codemod to automatically migrate:
npx jscodeshift@latest ./path/to/src/ \
  --extensions=js,jsx \
  --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
Run prettier and eslint after applying the codemod to fix formatting.

Query Callbacks Removed

onSuccess, onError, and onSettled callbacks have been removed from useQuery and QueryObserver. They remain available on mutations.
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {
    console.log('Data loaded', data)
  },
  onError: (error) => {
    console.error('Error loading data', error)
  },
})
See RFC #5279 for the motivation behind this change.

Renamed cacheTime to gcTime

The confusing cacheTime option has been renamed to gcTime (garbage collection time).
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 10 * 60 * 1000, // 10 minutes
    },
  },
})
cacheTime was misleading because it doesn’t control how long data is cached while in use. It only controls how long unused data stays in memory before being garbage collected.

Status Changes: loadingpending

The loading status and isLoading flag have been renamed:
const { data, isLoading, isInitialLoading } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

if (isLoading) return <Spinner />
For mutations:
const mutation = useMutation({
  mutationFn: addTodo,
})

// mutation.isPending (was mutation.isLoading)
// mutation.status === 'pending' (was 'loading')

TypeScript: Error is Default Error Type

Error type now defaults to Error instead of unknown:
const { error } = useQuery<Todo[], unknown>({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

if (error) {
  // error is unknown, needs type guard
  console.error(error)
}
To throw non-Error types:
useQuery<number, string>({
  queryKey: ['some-query'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'some error'
    return 42
  },
})

Minimum TypeScript Version: 4.7

TypeScript 4.7 or higher is now required for improved type inference.

Renamed useErrorBoundary to throwOnError

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  throwOnError: true, // was: useErrorBoundary: true
})

remove Method Removed from Query Result

const query = useQuery({ queryKey, queryFn })
query.remove() // Remove from cache

Removed keepPreviousData Option

Replaced with placeholderData identity function:
const { data, isPreviousData } = useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  keepPreviousData: true,
})
Or use a custom identity function:
placeholderData: (previousData) => previousData
placeholderData always gives you success status, while keepPreviousData gave you the previous query status.

Infinite Queries Require initialPageParam

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

Manual Infinite Query Mode Removed

You can no longer pass pageParam directly to fetchNextPage. The getNextPageParam value is always used.

null Now Indicates No More Pages

getNextPageParam and getPreviousPageParam can now return null (in addition to undefined) to indicate no more pages:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? null,
})

No Retries on Server by Default

Queries now default to retry: 0 on the server (was 3 in v4):
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: isServer ? 0 : 3,
    },
  },
})

refetchInterval Callback Simplified

refetchInterval: (data, query) => {
  if (data?.shouldPoll) return 1000
  return false
}

Window Focus Uses visibilitychange Only

The focus event is no longer used. Only visibilitychange is used for detecting window focus.

Network Status Detection Improved

navigator.onLine is no longer used initially. The online status is now determined by online and offline events only.

Custom Context Replaced with Custom QueryClient

const customContext = React.createContext<QueryClient | undefined>(undefined)

const { data } = useQuery(
  {
    queryKey: ['users', id],
    queryFn: () => fetch(...),
    context: customContext,
  }
)

refetchPage Removed, Use maxPages

Infinite queries now use maxPages to limit pages:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  maxPages: 3, // Only keep 3 pages in memory
})

isDataEqual Removed

Use structuralSharing function instead:
isDataEqual: (oldData, newData) => customCheck(oldData, newData)

Private Class Fields

TanStack Query now uses ECMAScript private class fields (#field). Previously “private” fields are now truly private at runtime.

Minimum React Version: 18.0

React 18 is now required for the useSyncExternalStore hook.

Hydrate Component Renamed to HydrationBoundary

import { Hydrate } from '@tanstack/react-query'

<Hydrate state={dehydratedState}>
  <App />
</Hydrate>

Query Defaults Now Merge

queryClient.setQueryDefaults() calls now merge instead of overriding. Order from most generic to most specific:
// Set generic defaults first
queryClient.setQueryDefaults(['todo'], {
  retry: false,
  staleTime: 60_000,
})

// Then specific defaults
queryClient.setQueryDefaults(['todo', 'detail'], {
  retry: true,
  retryDelay: 1_000,
  staleTime: 10_000,
})

hashQueryKey Renamed to hashKey

Now works with both query and mutation keys:
import { hashKey } from '@tanstack/react-query'

const hash = hashKey(['todos', 1])

New Features

Simplified Optimistic Updates

Use variables from mutation for optimistic UI:
const queryInfo = useTodos()
const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

return (
  <ul>
    {queryInfo.data?.items.map((todo) => (
      <li key={todo.id}>{todo.text}</li>
    ))}
    {addTodoMutation.isPending && (
      <li style={{ opacity: 0.5 }}>
        {addTodoMutation.variables}
      </li>
    )}
  </ul>
)

maxPages for Infinite Queries

Limit pages stored in memory:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  maxPages: 5, // Keep only 5 pages
})

Prefetch Multiple Pages

Infinite queries can prefetch multiple pages:
await queryClient.prefetchInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  pages: 3, // Prefetch first 3 pages
})

combine Option for useQueries

Combine results from multiple queries:
const result = useQueries({
  queries: [
    { queryKey: ['post', 1], queryFn: () => fetchPost(1) },
    { queryKey: ['post', 2], queryFn: () => fetchPost(2) },
  ],
  combine: (results) => {
    return {
      data: results.map((result) => result.data),
      pending: results.some((result) => result.isPending),
    }
  },
})

Suspense Hooks

Dedicated suspense hooks with better types:
const { data } = useSuspenseQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})

// data is never undefined!
Also available:
  • useSuspenseInfiniteQuery
  • useSuspenseQueries

Experimental Fine-Grained Persister

New experimental persister for better performance. See the createPersister docs.

Migration Checklist

1

Update Dependencies

npm install @tanstack/react-query@latest
npm install @tanstack/react-query-devtools@latest
2

Run Codemod

Apply the automated codemod for removing overloads.
3

Replace Callbacks

Move onSuccess, onError, onSettled from queries to useEffect.
4

Update Status Checks

Change isLoading to isPending where needed.
5

Rename Options

  • cacheTimegcTime
  • useErrorBoundarythrowOnError
  • keepPreviousDataplaceholderData: keepPreviousData
6

Update Infinite Queries

Add initialPageParam to all infinite queries.
7

Update Hydration

Rename Hydrate to HydrationBoundary.
8

Test Thoroughly

Test all query and mutation functionality.
Take your time with the migration. v5 brings significant improvements worth the effort!

Build docs developers (and LLMs) love