TanStack Query v4 - A Quick Look at What's Changed

A look at React Query updated to v4
24. 01. 2023 /
#react#javascript

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.

TQ4

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 about queryFn
    • 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 as fetchStatus:paused.
  • always: Attempt to fetch while offline as if you were online. It will have a status: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.

Interface and library changes

  • With queryKey in useQuery, 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 returning undefined, 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)


Written by Max Kim
Copyright © 2025 Jonghyuk Max Kim. All Right Reserved