TanStack Query v4 - A Quick Look at What's Changed
Over a year ago, I wrote a post exploring React Query. Time passed, v4 came out, and the name changed to Tanstack Query.
Here's a quick look at some of the major breaking changes based on the highlights from the official Tanstack Query documentation Migration to React Query 4. I'll be translating and paraphrasing from the docs and adding my own quirks.
The Idle State has been removed
In v4, status
, which was previously one of the return values of useQuery
, is split into fetchStatus
and status
.
status
:data
, indicates information about the query result.- loading: No data yet
- error: no data, but there is an error
- success: there is data
fetchStatus
: Indicates information aboutqueryFn
- idle: the query is not doing anything
- paused: The query is attempting to patch but has been paused. Associated with network mode
- fetching: The state that the query is patching
Doakes notes that both background refetch and stale-while-validate behavior can be described by a combination of these two states.
When there was only one status
, there were situations where the wording was ambiguous, which is why status
was split. The quotes below are taken from Doakes, and the corresponding ambiguities.
If the query is in success state, the fetchStatus should be idle, but it could be fetching because background patching is happening.
In v3, we had to use the isFetching
value in background refetch situations because we could not [properly know] that patching was happening by evaluating whether status
was loading
. This was because status
was not a good representation of the state of the query.
If there is no data since the query was mounted, it could be the case that the status is loading and the fetchStatus is fetching, but it could also be the case that it is paused, depending on the network connection.
In v3, if the first request fails, refetch is paused when there is no network connection. The status
remains loading
[until the connection resumes].(https://tkdodo.eu/blog/offline-react-query) In this situation, the status
does not indicate that the query is paused.
Queries and mutations, per default, need network connection to run
In v4, we added the networkMode
option, a feature that provides explicit offline mode for queries and mutations.
The networkMode:online
is provided by default in the QueryClient
setting, which indicates that there is no network connection via the fetchStatus:paused
described above when the network is offline.
If you want the behavior in v3, you can change the networkMode
setting to
new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst',
},
mutations: {
networkMode: 'offlineFirst',
},
},
});
There are three possible values for networkMode.
online
: No fetch is attempted while offline until a network connection is available, at which point the status of the query is marked asfetchStatus:paused
.always
: Attempt to fetch while offline as if you were online. It will have astatus:error
status because it's sending the request while offline.offlineFirst
: Same as behavior in v3: stop retrying after the first call to queryFn.
In the blog post by dox and tk-dodo, there's an interesting quadruple entendre when describing offline query.
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. (#)
I've said it time and time again - React Query is an async state manager. As long as you give it a Promise, resolved or rejected, the library is happy. Doesn't matter where that Promise comes from. (#)
network mode is a use case that is only related to data fetching, so I thought I'd explain that providing an option like networkMode
doesn't really fit the concept of "Async State Manager" that Tanstack Query stands for...
Query Filters
The active
and inactive
options are consolidated into a single type
that can be used with queryClient
to make bulk changes to queries that meet multiple conditions.
// Cancel all queries
await queryClient.cancelQueries();
// Remove all inactive queries that begin with `posts` in the key
queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' });
// Refetch all active queries
await queryClient.refetchQueries({ type: 'active' });
// Refetch all active queries that begin with `posts` in the key
await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' });
Originally, the active
and inactive
options were mutually exclusive and sometimes didn't work well when both were set. Setting both to false would match all queries, which is not expected behavior.
The type
attribute makes it easy to indicate whether the queries you're looking for are active
, inactive
, or all queries.
- active?: boolean
- inactive?: boolean
+ type?: 'active' | 'inactive' | 'all'
```diff active?
## onSuccess is no logger called from setQueryData
The `onSuccess` callback is no longer called when `setQueryData` is called. The `onSuccess` callback is only called after an actual request has been made.
With the old behavior, `setQueryData` could be called inside `onSuccess`, which could cause an infinite loop. Even when coupled with `staleTime`, `onSuccess` was not called when data was fetched from cache, which could cause logic to behave in unexpected ways.
Doakes also recommends using `useEffect` rather than `onSuccess` if you really want to subscribe to the data being changed: put the data in the `useEffect` dependency array.
```tsx
const { data } = useQuery({ queryKey, queryFn });
React.useEffect(() => mySideEffectHere(data), [data]);
With structural sharing, the effect below is not called on every background fetch, but only when the data really changes.
Tracked Query per default
A very cool update, Tracked Query becomes the default behavior. Tracked Query is an optimization that only re-renders the return value of a useQuery when the values it actually directly accesses change.
This was originally achieved by using notifyOnChangeProps
, one of the options in useQuery
, to manually specify which values to subscribe to.
// V3
function User() {
const { data } = useQuery('user', fetchUser, {
notifyOnChangeProps: ['data'], // only render when data changes
});
return <div>Username: {data.username}</div>;
}
Starting in v4, even without this processing, it will proxy the query on its own, determine which values are being accessed by the component, and subscribe to those values.
// V4
function User() {
// only render when data changes, even without notifyOnChangeProps
const { data } = useQuery('user', fetchUser);
return <div>Username: {data.username}</div>;
}
Now with the notifyOnChangeProps option, we don't need to pass an array of query return value names, we can just pass the 'all' value if we need to watch all the return values.
etc.
I don't think this is important, but it's worth a look.
queryClient.setQueryData(['todo', id], (previousTodo) =>
previousTodo ? { ...previousTodo, done: true } : undefined
);
When updating query data by passing a callback to setQueryData, if the callback returns undefined, the data will not be updated. This can be used when you need to update data conditionally, like in the example above.
- Consistent behavior for
cancelRefetch
: Provides optimization when updating query data imperatively. - PersistQueryClient and the corresponding persister plugins are no longer experimental and have been renamed: plugin to synchronize query data with external storage, no longer experimental.
- Mutation Cache Garbage Collection: Mutation can GC like Query and give a cacheTime, but I'm not sure how to keep the mutation cache and use it.
Interface and library changes
- With
queryKey
inuseQuery
, you can now only pass an array. #
- useQuery('todos', fetchTodos)
+ useQuery(['todos'], fetchTodos)
- There is a change in behavior when passing multiple queries to
useQueries
. #
- useQueries([{ queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }])
+ useQueries({ queries: [{ queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }] })
- Prevent
queryFn
from returningundefined
, simultaneously at type and runtime level. #
// ❌
useQuery(['key'], () => axios.get(url).then((result) => console.log(result.data)));
- Integrate the existing react-query/hydration package, which collects the APIs needed for SSR. #
- Support React18 as a first class. #
- Support for both ESM and CJS via the exports field. #
- Supports codemod for easy migration. #
(end)