File: migrating-to-react-query-3.md | Updated: 11/15/2025
Search...
+ K
Auto
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide
Documentation
Framework
React
Version
v5
Search...
+ K
Menu
Getting Started
Guides & Concepts
API Reference
ESLint
Examples
Plugins
Framework
React
Version
v5
Menu
Getting Started
Guides & Concepts
API Reference
ESLint
Examples
Plugins
On this page
ReactQueryConfigProvider and ReactQueryCacheProvider have both been replaced by QueryClientProvider
QueryCache.prefetchQuery() has been moved to QueryClient.prefetchQuery()
QueryCache.getQuery() has been replaced by QueryCache.find().
QueryCache.getQueries() has been moved to QueryCache.findAll().
QueryCache.isFetching has been moved to QueryClient.isFetching().
The useQueryCache hook has been replaced by the useQueryClient hook.
Query key parts/pieces are no longer automatically spread to the query function.
Infinite Query Page params are now passed via QueryFunctionContext.pageParam
usePaginatedQuery() has been removed in favor of the keepPreviousData option
Infinite Query data now contains the array of pages and pageParams used to fetch those pages.
If set, the QueryOptions.enabled option must be a boolean (true/false)
The QueryOptions.forceFetchOnMount option has been replaced by refetchOnMount: 'always'
The QueryResult.clear() function has been renamed to QueryResult.remove()
setConsole() has been replaced by the new setLogger() function
The useQueries() hook, for variable-length parallel query execution
Copy Markdown
Previous versions of React Query were awesome and brought some amazing new features, more magic, and an overall better experience to the library. They also brought on massive adoption and likewise a lot of refining fire (issues/contributions) to the library and brought to light a few things that needed more polish to make the library even better. v3 contains that very polish.
Breaking Changes
----------------
### The QueryCache has been split into a QueryClient and lower-level QueryCache and MutationCache instances.
The QueryCache contains all queries, the MutationCache contains all mutations, and the QueryClient can be used to set configuration and to interact with them.
This has some benefits:
When creating a new QueryClient(), a QueryCache and MutationCache are automatically created for you if you don't supply them.
tsx
import { QueryClient } from 'react-query'
const queryClient = new QueryClient()
import { QueryClient } from 'react-query'
const queryClient = new QueryClient()
Default options for queries and mutations can now be specified in QueryClient:
Notice that it's now defaultOptions instead of defaultConfig
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// query options
},
mutations: {
// mutation options
},
},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// query options
},
mutations: {
// mutation options
},
},
})
The QueryClientProvider component is now used to connect a QueryClient to your application:
tsx
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
### The default QueryCache is gone. For real this time!
As previously noted with a deprecation, there is no longer a default QueryCache that is created or exported from the main package. You must create your own via new QueryClient() or new QueryCache() (which you can then pass to new QueryClient({ queryCache }) )
### The deprecated makeQueryCache utility has been removed.
It's been a long time coming, but it's finally gone :)
### QueryCache.prefetchQuery() has been moved to QueryClient.prefetchQuery()
The new QueryClient.prefetchQuery() function is async, but does not return the data from the query. If you require the data, use the new QueryClient.fetchQuery() function
tsx
// Prefetch a query:
await queryClient.prefetchQuery('posts', fetchPosts)
// Fetch a query:
try {
const data = await queryClient.fetchQuery('posts', fetchPosts)
} catch (error) {
// Error handling
}
// Prefetch a query:
await queryClient.prefetchQuery('posts', fetchPosts)
// Fetch a query:
try {
const data = await queryClient.fetchQuery('posts', fetchPosts)
} catch (error) {
// Error handling
}
Together, these provide the same experience as before, but with added control to choose which component trees you want to reset. For more information, see:
### QueryCache.getQuery() has been replaced by QueryCache.find().
QueryCache.find() should now be used to look up individual queries from a cache
### QueryCache.getQueries() has been moved to QueryCache.findAll().
QueryCache.findAll() should now be used to look up multiple queries from a cache
### QueryCache.isFetching has been moved to QueryClient.isFetching().
Notice that it's now a function instead of a property
### The useQueryCache hook has been replaced by the useQueryClient hook.
It returns the provided queryClient for its component tree and shouldn't need much tweaking beyond a rename.
### Query key parts/pieces are no longer automatically spread to the query function.
Inline functions are now the suggested way of passing parameters to your query functions:
tsx
// Old
useQuery(['post', id], (_key, id) => fetchPost(id))
// New
useQuery(['post', id], () => fetchPost(id))
// Old
useQuery(['post', id], (_key, id) => fetchPost(id))
// New
useQuery(['post', id], () => fetchPost(id))
If you still insist on not using inline functions, you can use the newly passed QueryFunctionContext:
tsx
useQuery(['post', id], (context) => fetchPost(context.queryKey[1]))
useQuery(['post', id], (context) => fetchPost(context.queryKey[1]))
### Infinite Query Page params are now passed via QueryFunctionContext.pageParam
They were previously added as the last query key parameter in your query function, but this proved to be difficult for some patterns
tsx
// Old
useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))
// New
useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))
// Old
useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))
// New
useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))
### usePaginatedQuery() has been removed in favor of the keepPreviousData option
The new keepPreviousData options is available for both useQuery and useInfiniteQuery and will have the same "lagging" effect on your data:
tsx
import { useQuery } from 'react-query'
function Page({ page }) {
const { data } = useQuery(['page', page], fetchPage, {
keepPreviousData: true,
})
}
import { useQuery } from 'react-query'
function Page({ page }) {
const { data } = useQuery(['page', page], fetchPage, {
keepPreviousData: true,
})
}
### useInfiniteQuery() is now bi-directional
The useInfiniteQuery() interface has changed to fully support bi-directional infinite lists.
One direction:
tsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)
Both directions:
tsx
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
},
)
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
},
)
One direction reversed:
tsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)
### Infinite Query data now contains the array of pages and pageParams used to fetch those pages.
This allows for easier manipulation of the data and the page params, like, for example, removing the first page of data along with it's params:
tsx
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
### useMutation now returns an object instead of an array
Though the old way gave us warm fuzzy feelings of when we first discovered useState for the first time, they didn't last long. Now the mutation return is a single object.
tsx
// Old:
const [mutate, { status, reset }] = useMutation()
// New:
const { mutate, status, reset } = useMutation()
// Old:
const [mutate, { status, reset }] = useMutation()
// New:
const { mutate, status, reset } = useMutation()
### mutation.mutate no longer return a promise
We got a lot of questions regarding this behavior as users expected the promise to behave like a regular promise.
Because of this the mutate function is now split into a mutate and mutateAsync function.
The mutate function can be used when using callbacks:
tsx
const { mutate } = useMutation({ mutationFn: addTodo })
mutate('todo', {
onSuccess: (data) => {
console.log(data)
},
onError: (error) => {
console.error(error)
},
onSettled: () => {
console.log('settled')
},
})
const { mutate } = useMutation({ mutationFn: addTodo })
mutate('todo', {
onSuccess: (data) => {
console.log(data)
},
onError: (error) => {
console.error(error)
},
onSettled: () => {
console.log('settled')
},
})
The mutateAsync function can be used when using async/await:
tsx
const { mutateAsync } = useMutation({ mutationFn: addTodo })
try {
const data = await mutateAsync('todo')
console.log(data)
} catch (error) {
console.error(error)
} finally {
console.log('settled')
}
const { mutateAsync } = useMutation({ mutationFn: addTodo })
try {
const data = await mutateAsync('todo')
console.log(data)
} catch (error) {
console.error(error)
} finally {
console.log('settled')
}
### The object syntax for useQuery now uses a collapsed config:
tsx
// Old:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
config: { staleTime: Infinity },
})
// New:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
staleTime: Infinity,
})
// Old:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
config: { staleTime: Infinity },
})
// New:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
staleTime: Infinity,
})
### If set, the QueryOptions.enabled option must be a boolean (true/false)
The enabled query option will now only disable a query when the value is false. If needed, values can be casted with !!userId or Boolean(userId) and a handy error will be thrown if a non-boolean value is passed.
### The QueryOptions.initialStale option has been removed
The initialStale query option has been removed and initial data is now treated as regular data. Which means that if initialData is provided, the query will refetch on mount by default. If you do not want to refetch immediately, you can define a staleTime.
### The QueryOptions.forceFetchOnMount option has been replaced by refetchOnMount: 'always'
Honestly, we were accruing way too many refetchOn____ options, so this should clean things up.
When refetchOnMount was set to false any additional components were prevented from refetching on mount. In version 3 only the component where the option has been set will not refetch on mount.
The queryFnParamsFilter option has been removed because query functions now get a QueryFunctionContext object instead of the query key.
Parameters can still be filtered within the query function itself as the QueryFunctionContext also contains the query key.
With these new options it is possible to configure when a component should re-render on a granular level.
Only re-render when the data or error properties change:
tsx
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangeProps: ['data', 'error'],
})
return <div>Username: {data.username}</div>
}
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangeProps: ['data', 'error'],
})
return <div>Username: {data.username}</div>
}
Prevent re-render when the isStale property changes:
tsx
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangePropsExclusions: ['isStale'],
})
return <div>Username: {data.username}</div>
}
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangePropsExclusions: ['isStale'],
})
return <div>Username: {data.username}</div>
}
### The QueryResult.clear() function has been renamed to QueryResult.remove()
Although it was called clear, it really just removed the query from the cache. The name now matches the functionality.
Because data and errors can be present at the same time, the updatedAt property has been split into dataUpdatedAt and errorUpdatedAt.
### setConsole() has been replaced by the new setLogger() function
tsx
import { setLogger } from 'react-query'
// Log with Sentry
setLogger({
error: (error) => {
Sentry.captureException(error)
},
})
// Log with Winston
setLogger(winston.createLogger())
import { setLogger } from 'react-query'
// Log with Sentry
setLogger({
error: (error) => {
Sentry.captureException(error)
},
})
// Log with Winston
setLogger(winston.createLogger())
### React Native no longer requires overriding the logger
To prevent showing error screens in React Native when a query fails it was necessary to manually change the Console:
tsx
import { setConsole } from 'react-query'
setConsole({
log: console.log,
warn: console.warn,
error: console.warn,
})
import { setConsole } from 'react-query'
setConsole({
log: console.log,
warn: console.warn,
error: console.warn,
})
In version 3 this is done automatically when React Query is used in React Native.
enum to a union type
So, if you were checking the status property of a query or mutation against a QueryStatus enum property you will have to check it now against the string literal the enum previously held for each property.
Therefore you have to change the enum properties to their equivalent string literal, like this:
Here is an example of the changes you would have to make:
tsx
- import { useQuery, QueryStatus } from 'react-query';
+ import { useQuery } from 'react-query';
const { data, status } = useQuery(['post', id], () => fetchPost(id))
- if (status === QueryStatus.Loading) {
+ if (status === 'loading') {
...
}
- if (status === QueryStatus.Error) {
+ if (status === 'error') {
...
}
- import { useQuery, QueryStatus } from 'react-query';
+ import { useQuery } from 'react-query';
const { data, status } = useQuery(['post', id], () => fetchPost(id))
- if (status === QueryStatus.Loading) {
+ if (status === 'loading') {
...
}
- if (status === QueryStatus.Error) {
+ if (status === 'error') {
...
}
New features
------------
#### Query Data Selectors
The useQuery and useInfiniteQuery hooks now have a select option to select or transform parts of the query result.
tsx
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
select: (user) => user.username,
})
return <div>Username: {data}</div>
}
import { useQuery } from 'react-query'
function User() {
const { data } = useQuery(['user'], fetchUser, {
select: (user) => user.username,
})
return <div>Username: {data}</div>
}
Set the notifyOnChangeProps option to ['data', 'error'] to only re-render when the selected data changes.
#### The useQueries() hook, for variable-length parallel query execution
Wish you could run useQuery in a loop? The rules of hooks say no, but with the new useQueries() hook, you can!
tsx
import { useQueries } from 'react-query'
function Overview() {
const results = useQueries([\
{ queryKey: ['post', 1], queryFn: fetchPost },\
{ queryKey: ['post', 2], queryFn: fetchPost },\
])
return (
<ul>
{results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
</ul>
)
}
import { useQueries } from 'react-query'
function Overview() {
const results = useQueries([\
{ queryKey: ['post', 1], queryFn: fetchPost },\
{ queryKey: ['post', 2], queryFn: fetchPost },\
])
return (
<ul>
{results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
</ul>
)
}
By default React Query will not retry a mutation on error, but it is possible with the retry option:
tsx
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
If mutations fail because the device is offline, they will be retried in the same order when the device reconnects.
Mutations can now be persisted to storage and resumed at a later point. More information can be found in the mutations documentation.
A QueryObserver can be used to create and/or watch a query:
tsx
const observer = new QueryObserver(queryClient, { queryKey: 'posts' })
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
const observer = new QueryObserver(queryClient, { queryKey: 'posts' })
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
A InfiniteQueryObserver can be used to create and/or watch an infinite query:
tsx
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: 'posts',
queryFn: fetchPosts,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: 'posts',
queryFn: fetchPosts,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
A QueriesObserver can be used to create and/or watch multiple queries:
tsx
const observer = new QueriesObserver(queryClient, [\
{ queryKey: ['post', 1], queryFn: fetchPost },\
{ queryKey: ['post', 2], queryFn: fetchPost },\
])
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
const observer = new QueriesObserver(queryClient, [\
{ queryKey: ['post', 1], queryFn: fetchPost },\
{ queryKey: ['post', 2], queryFn: fetchPost },\
])
const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})
#### Set default options for specific queries
The QueryClient.setQueryDefaults() method can be used to set default options for specific queries:
tsx
queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })
function Component() {
const { data } = useQuery(['posts'])
}
queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })
function Component() {
const { data } = useQuery(['posts'])
}
#### Set default options for specific mutations
The QueryClient.setMutationDefaults() method can be used to set default options for specific mutations:
tsx
queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })
function Component() {
const { mutate } = useMutation({ mutationKey: ['addPost'] })
}
queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })
function Component() {
const { mutate } = useMutation({ mutationKey: ['addPost'] })
}
The useIsFetching() hook now accepts filters which can be used to for example only show a spinner for certain type of queries:
tsx
const fetches = useIsFetching({ queryKey: ['posts'] })
const fetches = useIsFetching({ queryKey: ['posts'] })
The core of React Query is now fully separated from React, which means it can also be used standalone or in other frameworks. Use the react-query/core entry point to only import the core functionality:
tsx
import { QueryClient } from 'react-query/core'
import { QueryClient } from 'react-query/core'
### Devtools are now part of the main repo and npm package
The devtools are now included in the react-query package itself under the import react-query/devtools. Simply replace react-query-devtools imports with react-query/devtools
Does this replace [Redux, MobX, etc]?
[###### Want to Skip the Docs?
Query.gg - The Official React Query Course
\
“If you’re serious about *really* understanding React Query, there’s no better way than with query.gg”—Tanner Linsley
Learn More](https://query.gg/?s=tanstack)
