v4 is a major version, so there are some breaking changes to be aware of:
You will need to un-/install dependencies and change the imports:
npm uninstall react-querynpm install @tanstack/react-querynpm install @tanstack/react-query-devtoolsnpm uninstall react-querynpm install @tanstack/react-querynpm install @tanstack/react-query-devtools- import { useQuery } from 'react-query' // [!code --]- import { ReactQueryDevtools } from 'react-query/devtools' // [!code --]+ import { useQuery } from '@tanstack/react-query' // [!code ++]+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // [!code ++]- import { useQuery } from 'react-query' // [!code --]- import { ReactQueryDevtools } from 'react-query/devtools' // [!code --]+ import { useQuery } from '@tanstack/react-query' // [!code ++]+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // [!code ++]To make the import migration easier, v4 comes with a codemod.
The codemod is a best efforts attempt to help you migrate the breaking change. Please review the generated code thoroughly! Also, there are edge cases that cannot be found by the code mod, so please keep an eye on the log output.
You can easily apply it by using one (or both) of the following commands:
If you want to run it against.js or.jsx files, please use the command below:
npx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.jsnpx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.jsIf you want to run it against.ts or.tsx files, please use the command below:
npx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.jsnpx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.jsPlease note in the case ofTypeScript you need to usetsx as the parser; otherwise, the codemod won't be applied properly!
Note: Applying the codemod might break your code formatting, so please don't forget to runprettier and/oreslint after you've applied the codemod!
Note: The codemod willonly change the imports - you still have to install the separate devtools package manually.
In v3, Query and Mutation Keys could be a String or an Array. Internally, React Query has always worked with Array Keys only, and we've sometimes exposed this to consumers. For example, in thequeryFn, you would always get the key as an Array to make working withDefault Query Functions easier.
However, we have not followed this concept through to all apis. For example, when using thepredicate function onQuery Filters you would get the raw Query Key. This makes it difficult to work with such functions if you use Query Keys that are mixed Arrays and Strings. The same was true when using global callbacks.
To streamline all apis, we've decided to make all keys Arrays only:
;-useQuery('todos', fetchTodos) + // [!code --] useQuery(['todos'], fetchTodos) // [!code ++];-useQuery('todos', fetchTodos) + // [!code --] useQuery(['todos'], fetchTodos) // [!code ++]To make this migration easier, we decided to deliver a codemod.
The codemod is a best efforts attempt to help you migrate the breaking change. Please review the generated code thoroughly! Also, there are edge cases that cannot be found by the code mod, so please keep an eye on the log output.
You can easily apply it by using one (or both) of the following commands:
If you want to run it against.js or.jsx files, please use the command below:
npx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.jsnpx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.jsIf you want to run it against.ts or.tsx files, please use the command below:
npx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.jsnpx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.jsPlease note in the case ofTypeScript you need to usetsx as the parser; otherwise, the codemod won't be applied properly!
Note: Applying the codemod might break your code formatting, so please don't forget to runprettier and/oreslint after you've applied the codemod!
With the introduction of the newfetchStatus for better offline support, theidle state became irrelevant, becausefetchStatus: 'idle' captures the same state better. For more information, please readWhy two different states.
This will mostly affectdisabled queries that don't have anydata yet, as those were inidle state before:
- status: 'idle' // [!code --]+ status: 'loading' // [!code ++]+ fetchStatus: 'idle' // [!code ++]- status: 'idle' // [!code --]+ status: 'loading' // [!code ++]+ fetchStatus: 'idle' // [!code ++]Also, have a look atthe guide on dependent queries
Due to this change, disabled queries (even temporarily disabled ones) will start inloading state. To make migration easier, especially for having a good flag to know when to display a loading spinner, you can check forisInitialLoading instead ofisLoading:
;-isLoading + // [!code --] isInitialLoading // [!code ++];-isLoading + // [!code --] isInitialLoading // [!code ++]See also the guide ondisabling queries
TheuseQueries hook now accepts an object with aqueries prop as its input. The value of thequeries prop is an array of queries (this array is identical to what was passed intouseQueries in v3).
;-useQueries([ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 },]) + // [!code --] useQueries({ queries: [ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }, ], }) // [!code ++];-useQueries([ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 },]) + // [!code --] useQueries({ queries: [ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }, ], }) // [!code ++]In order to make bailing out of updates possible by returningundefined, we had to makeundefined an illegal cache value. This is in-line with other concepts of react-query, for example, returningundefined from theinitialData function will alsonot set data.
Further, it is an easy bug to producePromise<void> by adding logging in the queryFn:
useQuery(['key'], () => axios.get(url).then((result) => console.log(result.data)),)useQuery(['key'], () => axios.get(url).then((result) => console.log(result.data)),)This is now disallowed on type level; at runtime,undefined will be transformed to afailed Promise, which means you will get anerror, which will also be logged to the console in development mode.
Please read theNew Features announcement about online / offline support, and also the dedicated page aboutNetwork mode
Even though React Query is an Async State Manager that can be used for anything that produces a Promise, it is most often used for data fetching in combination with data fetching libraries. That is why, per default, queries and mutations will bepaused if there is no network connection. If you want to opt-in to the previous behavior, you can globally setnetworkMode: offlineFirst for both queries and mutations:
new QueryClient({ defaultOptions: { queries: { networkMode: 'offlineFirst', }, mutations: { networkMode: 'offlineFirst', }, },})new QueryClient({ defaultOptions: { queries: { networkMode: 'offlineFirst', }, mutations: { networkMode: 'offlineFirst', }, },})ThenotifyOnChangeProps option no longer accepts a"tracked" value. Instead,useQuery defaults to tracking properties. All queries usingnotifyOnChangeProps: "tracked" should be updated by removing this option.
If you would like to bypass this in any queries to emulate the v3 default behavior of re-rendering whenever a query changes,notifyOnChangeProps now accepts an"all" value to opt-out of the default smart tracking optimization.
In v4,notifyOnChangeProps defaults to the"tracked" behavior of v3 instead ofundefined. Now that"tracked" is the default behavior for v4, it no longer makes sense to include this config option.
ThecancelRefetch option can be passed to all functions that imperatively fetch a query, namely:
Except forfetchNextPage andfetchPreviousPage, this flag was defaulting tofalse, which was inconsistent and potentially troublesome: CallingrefetchQueries orinvalidateQueries after a mutation might not yield the latest result if a previous slow fetch was already ongoing, because this refetch would have been skipped.
We believe that if a query is actively refetched by some code you write, it should, per default, re-start the fetch.
That is why this flag now defaults totrue for all methods mentioned above. It also means that if you callrefetchQueries twice in a row, without awaiting it, it will now cancel the first fetch and re-start it with the second one:
queryClient.refetchQueries({ queryKey: ['todos'] })// this will abort the previous refetch and start a new fetchqueryClient.refetchQueries({ queryKey: ['todos'] })queryClient.refetchQueries({ queryKey: ['todos'] })// this will abort the previous refetch and start a new fetchqueryClient.refetchQueries({ queryKey: ['todos'] })You can opt-out of this behaviour by explicitly passingcancelRefetch:false:
queryClient.refetchQueries({ queryKey: ['todos'] })// this will not abort the previous refetch - it will just be ignoredqueryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })queryClient.refetchQueries({ queryKey: ['todos'] })// this will not abort the previous refetch - it will just be ignoredqueryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })Note: There is no change in behaviour for automatically triggered fetches, e.g. because a query mounts or because of a window focus refetch.
Aquery filter is an object with certain conditions to match a query. Historically, the filter options have mostly been a combination of boolean flags. However, combining those flags can lead to impossible states. Specifically:
active?: boolean - When set to true it will match active queries. - When set to false it will match inactive queries.inactive?: boolean - When set to true it will match inactive queries. - When set to false it will match active queries.active?: boolean - When set to true it will match active queries. - When set to false it will match inactive queries.inactive?: boolean - When set to true it will match inactive queries. - When set to false it will match active queries.Those flags don't work well when used together, because they are mutually exclusive. Settingfalse for both flags could match all queries, judging from the description, or no queries, which doesn't make much sense.
With v4, those filters have been combined into a single filter to better show the intent:
- active?: boolean // [!code --]- inactive?: boolean // [!code --]+ type?: 'active' | 'inactive' | 'all' // [!code ++]- active?: boolean // [!code --]- inactive?: boolean // [!code --]+ type?: 'active' | 'inactive' | 'all' // [!code ++]The filter defaults toall, and you can choose to only matchactive orinactive queries.
queryClient.invalidateQueries had two additional, similar flags:
refetchActive: Boolean - Defaults to true - When set to false, queries that match the refetch predicate and are actively being rendered via useQuery and friends will NOT be refetched in the background, and only marked as invalid.refetchInactive: Boolean - Defaults to false - When set to true, queries that match the refetch predicate and are not being rendered via useQuery and friends will be both marked as invalid and also refetched in the backgroundrefetchActive: Boolean - Defaults to true - When set to false, queries that match the refetch predicate and are actively being rendered via useQuery and friends will NOT be refetched in the background, and only marked as invalid.refetchInactive: Boolean - Defaults to false - When set to true, queries that match the refetch predicate and are not being rendered via useQuery and friends will be both marked as invalid and also refetched in the backgroundFor the same reason, those have also been combined:
- refetchActive?: boolean // [!code --]- refetchInactive?: boolean // [!code --]+ refetchType?: 'active' | 'inactive' | 'all' | 'none' // [!code ++]- refetchActive?: boolean // [!code --]- refetchInactive?: boolean // [!code --]+ refetchType?: 'active' | 'inactive' | 'all' | 'none' // [!code ++]This flag defaults toactive becauserefetchActive defaulted totrue. This means we also need a way to tellinvalidateQueries to not refetch at all, which is why a fourth option (none) is also allowed here.
This was confusing to many and also created infinite loops ifsetQueryData was called from withinonSuccess. It was also a frequent source of error when combined withstaleTime, because if data was read from the cache only,onSuccess wasnot called.
Similar toonError andonSettled, theonSuccess callback is now tied to a request being made. No request -> no callback.
If you want to listen to changes of thedata field, you can best do this with auseEffect, wheredata is part of the dependency Array. Since React Query ensures stable data through structural sharing, the effect will not execute with every background refetch, but only if something within data has changed:
const { data } = useQuery({ queryKey, queryFn })React.useEffect(() => mySideEffectHere(data), [data])const { data } = useQuery({ queryKey, queryFn })React.useEffect(() => mySideEffectHere(data), [data])The pluginscreateWebStoragePersistor andcreateAsyncStoragePersistor have been renamed tocreateSyncStoragePersister andcreateAsyncStoragePersister respectively. The interfacePersistor inpersistQueryClient has also been renamed toPersister. Checkoutthis stackexchange for the motivation of this change.
Since these plugins are no longer experimental, their import paths have also been updated:
- import { persistQueryClient } from 'react-query/persistQueryClient-experimental' // [!code --]- import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental' // [!code --]- import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental' // [!code --]+ import { persistQueryClient } from '@tanstack/react-query-persist-client' // [!code ++]+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' // [!code ++]+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' // [!code ++]- import { persistQueryClient } from 'react-query/persistQueryClient-experimental' // [!code --]- import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental' // [!code --]- import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental' // [!code --]+ import { persistQueryClient } from '@tanstack/react-query-persist-client' // [!code ++]+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' // [!code ++]+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' // [!code ++]Theoldcancel method that allowed you to define acancel function on promises, which was then used by the library to support query cancellation, has been removed. We recommend to use thenewer API (introduced with v3.30.0) for query cancellation that uses theAbortController API internally and provides you with anAbortSignal instance for your query function to support query cancellation.
Types now require using TypeScript v4.1 or greater
As of v4, React Query is optimized for modern browsers. We have updated our browserslist to produce a more modern, performant and smaller bundle. You can read about the requirementshere.
It was possible to change the logger globally by callingsetLogger. In v4, that function is replaced with an optional field when creating aQueryClient.
- import { QueryClient, setLogger } from 'react-query'; // [!code --]+ import { QueryClient } from '@tanstack/react-query'; // [!code ++]- setLogger(customLogger) // [!code --]- const queryClient = new QueryClient(); // [!code --]+ const queryClient = new QueryClient({ logger: customLogger }) // [!code ++]- import { QueryClient, setLogger } from 'react-query'; // [!code --]+ import { QueryClient } from '@tanstack/react-query'; // [!code ++]- setLogger(customLogger) // [!code --]- const queryClient = new QueryClient(); // [!code --]+ const queryClient = new QueryClient({ logger: customLogger }) // [!code ++]In v3, React Query would cache query results for a default of 5 minutes, then manually garbage collect that data. This default was applied to server-side React Query as well.
This lead to high memory consumption and hanging processes waiting for this manual garbage collection to complete. In v4, by default the server-sidecacheTime is now set toInfinity effectively disabling manual garbage collection (the NodeJS process will clear everything once a request is complete).
This change only impacts users of server-side React Query, such as with Next.js. If you are setting acacheTime manually this will not impact you (although you may want to mirror behavior).
Starting with v4, react-query will no longer log errors (e.g. failed fetches) to the console in production mode, as this was confusing to many.Errors will still show up in development mode.
React Query now supportspackage.json"exports" and is fully compatible with Node's native resolution for both CommonJS and ESM. We don't expect this to be a breaking change for most users, but this restricts the files you can import into your project to only the entry points we officially support.
Subscribing manually to theQueryCache has always given you aQueryCacheNotifyEvent, but this was not true for theMutationCache. We have streamlined the behavior and also adapted event names accordingly.
- type: 'queryAdded' // [!code --]+ type: 'added' // [!code ++]- type: 'queryRemoved' // [!code --]+ type: 'removed' // [!code ++]- type: 'queryUpdated' // [!code --]+ type: 'updated' // [!code ++]- type: 'queryAdded' // [!code --]+ type: 'added' // [!code ++]- type: 'queryRemoved' // [!code --]+ type: 'removed' // [!code ++]- type: 'queryUpdated' // [!code --]+ type: 'updated' // [!code ++]TheMutationCacheNotifyEvent uses the same types as theQueryCacheNotifyEvent.
Note: This is only relevant if you manually subscribe to the caches viaqueryCache.subscribe ormutationCache.subscribe
With version3.22.0, hydration utilities moved into the React Query core. With v3, you could still use the old exports fromreact-query/hydration, but these exports have been removed with v4.
- import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration' // [!code --]+ import { dehydrate, hydrate, useHydrate, Hydrate } from '@tanstack/react-query' // [!code ++]- import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration' // [!code --]+ import { dehydrate, hydrate, useHydrate, Hydrate } from '@tanstack/react-query' // [!code ++]The methodscancelMutations andexecuteMutation on theQueryClient were undocumented and unused internally, so we removed them. Since it was just a wrapper around a method available on themutationCache, you can still use the functionality ofexecuteMutation
- executeMutation< // [!code --]- TData = unknown, // [!code --]- TError = unknown, // [!code --]- TVariables = void, // [!code --]- TContext = unknown // [!code --]- >( // [!code --]- options: MutationOptions<TData, TError, TVariables, TContext> // [!code --]- ): Promise<TData> { // [!code --]- return this.mutationCache.build(this, options).execute() // [!code --]- } // [!code --]- executeMutation< // [!code --]- TData = unknown, // [!code --]- TError = unknown, // [!code --]- TVariables = void, // [!code --]- TContext = unknown // [!code --]- >( // [!code --]- options: MutationOptions<TData, TError, TVariables, TContext> // [!code --]- ): Promise<TData> { // [!code --]- return this.mutationCache.build(this, options).execute() // [!code --]- } // [!code --]Additionally,query.setDefaultOptions was removed because it was also unused.mutation.cancel was removed because it didn't actually cancel the outgoing request.
Previously, React Query had a directory namedreact which imported from thereact module. This could cause problems with some Jest configurations, resulting in errors when running tests like:
TypeError: Cannot read property 'createContext' of undefinedTypeError: Cannot read property 'createContext' of undefinedWith the renamed directory this no longer is an issue.
If you were importing anything from'react-query/react' directly in your project (as opposed to just'react-query'), then you need to update your imports:
- import { QueryClientProvider } from 'react-query/react'; // [!code --]+ import { QueryClientProvider } from '@tanstack/react-query/reactjs'; // [!code ++]- import { QueryClientProvider } from 'react-query/react'; // [!code --]+ import { QueryClientProvider } from '@tanstack/react-query/reactjs'; // [!code ++]v4 comes with an awesome set of new features:
React 18 was released earlier this year, and v4 now has first class support for it and the new concurrent features it brings.
In v3, React Query has always fired off queries and mutations, but then taken the assumption that if you want to retry it, you need to be connected to the internet. This has led to several confusing situations:
With v4, React Query introduces a newnetworkMode to tackle all these issues. Please read the dedicated page about the newNetwork mode for more information.
React Query defaults to "tracking" query properties, which should give you a nice boost in render optimization. The feature has existed sincev3.6.0 and has now become the default behavior with v4.
When using thefunctional updater form of setQueryData, you can now bail out of the update by returningundefined. This is helpful ifundefined is given to you aspreviousValue, which means that currently, no cached entry exists and you don't want to / cannot create one, like in the example of toggling a todo:
queryClient.setQueryData(['todo', id], (previousTodo) => previousTodo ? { ...previousTodo, done: true } : undefined,)queryClient.setQueryData(['todo', id], (previousTodo) => previousTodo ? { ...previousTodo, done: true } : undefined,)Mutations can now also be garbage collected automatically, just like queries. The defaultcacheTime for mutations is also set to 5 minutes.
Custom contexts can now be specified to pair hooks with their matchingProvider. This is critical when there may be multiple React QueryProvider instances in the component tree, and you need to ensure your hook uses the correctProvider instance.
An example:
// Our first data package: @my-scope/container-dataconst context = React.createContext<QueryClient | undefined>(undefined)const queryClient = new QueryClient()export const useUser = () => { return useQuery(USER_KEY, USER_FETCHER, { context, })}export const ContainerDataProvider = ({ children,}: { children: React.ReactNode}) => { return ( <QueryClientProvider client={queryClient} context={context}> {children} </QueryClientProvider> )}// Our first data package: @my-scope/container-dataconst context = React.createContext<QueryClient | undefined>(undefined)const queryClient = new QueryClient()export const useUser = () => { return useQuery(USER_KEY, USER_FETCHER, { context, })}export const ContainerDataProvider = ({ children,}: { children: React.ReactNode}) => { return ( <QueryClientProvider client={queryClient} context={context}> {children} </QueryClientProvider> )}// Our second data package: @my-scope/my-component-dataconst context = React.createContext<QueryClient | undefined>(undefined)const queryClient = new QueryClient()export const useItems = () => { return useQuery(ITEMS_KEY, ITEMS_FETCHER, { context, })}export const MyComponentDataProvider = ({ children,}: { children: React.ReactNode}) => { return ( <QueryClientProvider client={queryClient} context={context}> {children} </QueryClientProvider> )}// Our second data package: @my-scope/my-component-dataconst context = React.createContext<QueryClient | undefined>(undefined)const queryClient = new QueryClient()export const useItems = () => { return useQuery(ITEMS_KEY, ITEMS_FETCHER, { context, })}export const MyComponentDataProvider = ({ children,}: { children: React.ReactNode}) => { return ( <QueryClientProvider client={queryClient} context={context}> {children} </QueryClientProvider> )}// Our applicationimport { ContainerDataProvider, useUser } from "@my-scope/container-data";import { AppDataProvider } from "@my-scope/app-data";import { MyComponentDataProvider, useItems } from "@my-scope/my-component-data";<ContainerDataProvider> // <-- Provides container data (like "user") using its own React Query provider ... <AppDataProvider> // <-- Provides app data using its own React Query provider (unused in this example) ... <MyComponentDataProvider> // <-- Provides component data (like "items") using its own React Query provider <MyComponent /> </MyComponentDataProvider> ... </AppDataProvider> ...</ContainerDataProvider>// Example of hooks provided by the "DataProvider" components above:const MyComponent = () => { const user = useUser() // <-- Uses the context specified in ContainerDataProvider. const items = useItems() // <-- Uses the context specified in MyComponentDataProvider ...}// Our applicationimport { ContainerDataProvider, useUser } from "@my-scope/container-data";import { AppDataProvider } from "@my-scope/app-data";import { MyComponentDataProvider, useItems } from "@my-scope/my-component-data";<ContainerDataProvider> // <-- Provides container data (like "user") using its own React Query provider ... <AppDataProvider> // <-- Provides app data using its own React Query provider (unused in this example) ... <MyComponentDataProvider> // <-- Provides component data (like "items") using its own React Query provider <MyComponent /> </MyComponentDataProvider> ... </AppDataProvider> ...</ContainerDataProvider>// Example of hooks provided by the "DataProvider" components above:const MyComponent = () => { const user = useUser() // <-- Uses the context specified in ContainerDataProvider. const items = useItems() // <-- Uses the context specified in MyComponentDataProvider ...}