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.
Overview #
- More scalable and testable cache configuration
- Better SSR support
- Data-lag (previously usePaginatedQuery) anywhere!
- Bi-directional Infinite Queries
- Query data selectors!
- Fully configure defaults for queries and/or mutations before use
- More granularity for optional rendering optimization
- New
useQueries
hook! (Variable-length parallel query execution) - Query filter support for the
useIsFetching()
hook! - Retry/offline/replay support for mutations
- Observe queries/mutations outside of React
- Use the React Query core logic anywhere you want!
- Bundled/Colocated Devtools via
react-query/devtools
- Cache Persistence to web storage (experimental via
react-query/persistQueryClient-experimental
andreact-query/createWebStoragePersistor-experimental
)
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:
- Allows for different types of caches.
- Multiple clients with different configurations can use the same cache.
- Clients can be used to track queries, which can be used for shared caches on SSR.
- The client API is more focused towards general usage.
- Easier to test the individual components.
When creating a new QueryClient()
, a QueryCache
and MutationCache
are automatically created for you if you don't supply them.
1import { QueryClient } from 'react-query'
2
3const queryClient = new QueryClient()
ReactQueryConfigProvider
and ReactQueryCacheProvider
have both been replaced by QueryClientProvider
#
Default options for queries and mutations can now be specified in QueryClient
:
Notice that it's now defaultOptions instead of defaultConfig
1const queryClient = new QueryClient({
2 defaultOptions: {
3 queries: {
4 // query options
5 },
6 mutations: {
7 // mutation options
8 },
9 },
10})
The QueryClientProvider
component is now used to connect a QueryClient
to your application:
1import { QueryClient, QueryClientProvider } from 'react-query'
2
3const queryClient = new QueryClient()
4
5function App() {
6 return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
7}
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
1// Prefetch a query:
2await queryClient.prefetchQuery('posts', fetchPosts)
3
4// Fetch a query:
5try {
6 const data = await queryClient.fetchQuery('posts', fetchPosts)
7} catch (error) {
8 // Error handling
9}
ReactQueryErrorResetBoundary
and QueryCache.resetErrorBoundaries()
have been replaced by QueryErrorResetBoundary
and useQueryErrorResetBoundary()
. #
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:
1// Old
2useQuery(['post', id], (_key, id) => fetchPost(id))
3
4// New
5useQuery(['post', id], () => fetchPost(id))
If you still insist on not using inline functions, you can use the newly passed QueryFunctionContext
:
1useQuery(['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
1// Old
2useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))
3
4// New
5useInfiniteQuery(['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:
1import { useQuery } from 'react-query'
2
3function Page({ page }) {
4 const { data } = useQuery(['page', page], fetchPage, {
5 keepPreviousData: true,
6 })
7}
useInfiniteQuery() is now bi-directional #
The useInfiniteQuery()
interface has changed to fully support bi-directional infinite lists.
options.getFetchMore
has been renamed tooptions.getNextPageParam
queryResult.canFetchMore
has been renamed toqueryResult.hasNextPage
queryResult.fetchMore
has been renamed toqueryResult.fetchNextPage
queryResult.isFetchingMore
has been renamed toqueryResult.isFetchingNextPage
- Added the
options.getPreviousPageParam
option - Added the
queryResult.hasPreviousPage
property - Added the
queryResult.fetchPreviousPage
property - Added the
queryResult.isFetchingPreviousPage
- The
data
of an infinite query is now an object containing thepages
and thepageParams
used to fetch the pages:{ pages: [data, data, data], pageParams: [...]}
One direction:
1const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
2 useInfiniteQuery(
3 'projects',
4 ({ pageParam = 0 }) => fetchProjects(pageParam),
5 {
6 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
7 },
8 )
Both directions:
1const {
2 data,
3 fetchNextPage,
4 fetchPreviousPage,
5 hasNextPage,
6 hasPreviousPage,
7 isFetchingNextPage,
8 isFetchingPreviousPage,
9} = useInfiniteQuery(
10 'projects',
11 ({ pageParam = 0 }) => fetchProjects(pageParam),
12 {
13 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
14 getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
15 },
16)
One direction reversed:
1const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
2 useInfiniteQuery(
3 'projects',
4 ({ pageParam = 0 }) => fetchProjects(pageParam),
5 {
6 select: (data) => ({
7 pages: [...data.pages].reverse(),
8 pageParams: [...data.pageParams].reverse(),
9 }),
10 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
11 },
12 )
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:
1queryClient.setQueryData(['projects'], (data) => ({
2 pages: data.pages.slice(1),
3 pageParams: data.pageParams.slice(1),
4}))
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.
1// Old:
2const [mutate, { status, reset }] = useMutation()
3
4// New:
5const { mutate, status, reset } = useMutation()
mutation.mutate
no longer return a promise #
- The
[mutate]
variable has been changed to themutation.mutate
function - Added the
mutation.mutateAsync
function
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:
1const { mutate } = useMutation({ mutationFn: addTodo })
2
3mutate('todo', {
4 onSuccess: (data) => {
5 console.log(data)
6 },
7 onError: (error) => {
8 console.error(error)
9 },
10 onSettled: () => {
11 console.log('settled')
12 },
13})
The mutateAsync
function can be used when using async/await:
1const { mutateAsync } = useMutation({ mutationFn: addTodo })
2
3try {
4 const data = await mutateAsync('todo')
5 console.log(data)
6} catch (error) {
7 console.error(error)
8} finally {
9 console.log('settled')
10}
The object syntax for useQuery now uses a collapsed config: #
1// Old:
2useQuery({
3 queryKey: 'posts',
4 queryFn: fetchPosts,
5 config: { staleTime: Infinity },
6})
7
8// New:
9useQuery({
10 queryKey: 'posts',
11 queryFn: fetchPosts,
12 staleTime: Infinity,
13})
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.
The QueryOptions.refetchOnMount
options now only applies to its parent component instead of all query observers #
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 QueryOptions.queryFnParamsFilter
has been removed in favor of the new QueryFunctionContext
object. #
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.
The QueryOptions.notifyOnStatusChange
option has been superseded by the new notifyOnChangeProps
and notifyOnChangePropsExclusions
options. #
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:
1import { useQuery } from 'react-query'
2
3function User() {
4 const { data } = useQuery(['user'], fetchUser, {
5 notifyOnChangeProps: ['data', 'error'],
6 })
7 return <div>Username: {data.username}</div>
8}
Prevent re-render when the isStale
property changes:
1import { useQuery } from 'react-query'
2
3function User() {
4 const { data } = useQuery(['user'], fetchUser, {
5 notifyOnChangePropsExclusions: ['isStale'],
6 })
7 return <div>Username: {data.username}</div>
8}
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.
The QueryResult.updatedAt
property has been split into QueryResult.dataUpdatedAt
and QueryResult.errorUpdatedAt
properties #
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 #
1import { setLogger } from 'react-query'
2
3// Log with Sentry
4setLogger({
5 error: (error) => {
6 Sentry.captureException(error)
7 },
8})
9
10// Log with Winston
11setLogger(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:
1import { setConsole } from 'react-query'
2
3setConsole({
4 log: console.log,
5 warn: console.warn,
6 error: console.warn,
7})
In version 3 this is done automatically when React Query is used in React Native.
Typescript #
QueryStatus
has been changed from an 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:
QueryStatus.Idle
->'idle'
QueryStatus.Loading
->'loading'
QueryStatus.Error
->'error'
QueryStatus.Success
->'success'
Here is an example of the changes you would have to make:
1- import { useQuery, QueryStatus } from 'react-query'; // [!code --]
2+ import { useQuery } from 'react-query'; // [!code ++]
3
4const { data, status } = useQuery(['post', id], () => fetchPost(id))
5
6- if (status === QueryStatus.Loading) { // [!code --]
7+ if (status === 'loading') { // [!code ++]
8 ...
9}
10
11- if (status === QueryStatus.Error) { // [!code --]
12+ if (status === 'error') { // [!code ++]
13 ...
14}
New features #
Query Data Selectors #
The useQuery
and useInfiniteQuery
hooks now have a select
option to select or transform parts of the query result.
1import { useQuery } from 'react-query'
2
3function User() {
4 const { data } = useQuery(['user'], fetchUser, {
5 select: (user) => user.username,
6 })
7 return <div>Username: {data}</div>
8}
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!
1import { useQueries } from 'react-query'
2
3function Overview() {
4 const results = useQueries([
5 { queryKey: ['post', 1], queryFn: fetchPost },
6 { queryKey: ['post', 2], queryFn: fetchPost },
7 ])
8 return (
9 <ul>
10 {results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
11 </ul>
12 )
13}
Retry/offline mutations #
By default React Query will not retry a mutation on error, but it is possible with the retry
option:
1const mutation = useMutation({
2 mutationFn: addTodo,
3 retry: 3,
4})
If mutations fail because the device is offline, they will be retried in the same order when the device reconnects.
Persist mutations #
Mutations can now be persisted to storage and resumed at a later point. More information can be found in the mutations documentation.
QueryObserver #
A QueryObserver
can be used to create and/or watch a query:
1const observer = new QueryObserver(queryClient, { queryKey: 'posts' })
2
3const unsubscribe = observer.subscribe((result) => {
4 console.log(result)
5 unsubscribe()
6})
InfiniteQueryObserver #
A InfiniteQueryObserver
can be used to create and/or watch an infinite query:
1const observer = new InfiniteQueryObserver(queryClient, {
2 queryKey: 'posts',
3 queryFn: fetchPosts,
4 getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
5 getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
6})
7
8const unsubscribe = observer.subscribe((result) => {
9 console.log(result)
10 unsubscribe()
11})
QueriesObserver #
A QueriesObserver
can be used to create and/or watch multiple queries:
1const observer = new QueriesObserver(queryClient, [
2 { queryKey: ['post', 1], queryFn: fetchPost },
3 { queryKey: ['post', 2], queryFn: fetchPost },
4])
5
6const unsubscribe = observer.subscribe((result) => {
7 console.log(result)
8 unsubscribe()
9})
Set default options for specific queries #
The QueryClient.setQueryDefaults()
method can be used to set default options for specific queries:
1queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })
2
3function Component() {
4 const { data } = useQuery(['posts'])
5}
Set default options for specific mutations #
The QueryClient.setMutationDefaults()
method can be used to set default options for specific mutations:
1queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })
2
3function Component() {
4 const { mutate } = useMutation({ mutationKey: ['addPost'] })
5}
useIsFetching() #
The useIsFetching()
hook now accepts filters which can be used to for example only show a spinner for certain type of queries:
1const fetches = useIsFetching({ queryKey: ['posts'] })
Core separation #
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:
1import { 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