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.
Before (v4)
After (v5) - Use useEffect
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: loading → pending
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
Update Dependencies
npm install @tanstack/react-query@latest
npm install @tanstack/react-query-devtools@latest
Run Codemod
Apply the automated codemod for removing overloads.
Replace Callbacks
Move onSuccess, onError, onSettled from queries to useEffect.
Update Status Checks
Change isLoading to isPending where needed.
Rename Options
cacheTime → gcTime
useErrorBoundary → throwOnError
keepPreviousData → placeholderData: keepPreviousData
Update Infinite Queries
Add initialPageParam to all infinite queries.
Update Hydration
Rename Hydrate to HydrationBoundary.
Test Thoroughly
Test all query and mutation functionality.
Take your time with the migration. v5 brings significant improvements worth the effort!