---
title: "Migrating to TanStack Query v5"
description:
  "Key points and breaking changes to consider when migrating to TanStack Query
  v5."
canonical_url: "https://www.bigbinary.com/blog/migrating-to-tanstack-query-v5"
markdown_url: "https://www.bigbinary.com/blog/migrating-to-tanstack-query-v5.md"
---

# Migrating to TanStack Query v5

Key points and breaking changes to consider when migrating to TanStack Query v5.

- Author: Gaagul C Gigi
- Published: March 11, 2025
- Categories: React

[TanStack Query](https://tanstack.com/query/latest) is a powerful data-fetching
and state management library. Since the release of TanStack Query v5, many
developers upgrading to the new version have faced challenges in migrating their
existing functionality. While the official documentation covers all the details,
it can be overwhelming, making it easy to miss important updates.

In this blog, we’ll explain the main updates in TanStack Query v5 and show how
to make the switch smoothly.

For a complete list of changes, check out the
[TanStack Query v5 Migration Guide](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5).

## [Simplified Function Signatures](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#supports-a-single-signature-one-object)

In previous versions of React Query, functions like `useQuery` and `useMutation`
had multiple type overloads. This not only made type maintenance more
complicated but also led to the need for runtime checks to validate the types of
parameters.

To streamline the API, TanStack Query v5 introduces a simplified approach: a
single parameter as an object containing the main parameters for each function.

- queryKey / mutationKey
- queryFn / mutationFn
- …options

Below are some examples of how commonly used hooks and `queryClient` methods
have been restructured.

- Hooks

```javascript
// before (Multiple overloads)
useQuery(key, fn, options);
useInfiniteQuery(key, fn, options);
useMutation(fn, options);
useIsFetching(key, filters);
useIsMutating(key, filters);

// after (Single object parameter)
useQuery({ queryKey, queryFn, ...options });
useInfiniteQuery({ queryKey, queryFn, ...options });
useMutation({ mutationFn, ...options });
useIsFetching({ queryKey, ...filters });
useIsMutating({ mutationKey, ...filters });
```

- `queryClient` Methods:

```javascript
// before (Multiple overloads)
queryClient.isFetching(key, filters);
queryClient.getQueriesData(key, filters);
queryClient.setQueriesData(key, updater, filters, options);
queryClient.removeQueries(key, filters);
queryClient.cancelQueries(key, filters, options);
queryClient.invalidateQueries(key, filters, options);

// after (Single object parameter)
queryClient.isFetching({ queryKey, ...filters });
queryClient.getQueriesData({ queryKey, ...filters });
queryClient.setQueriesData({ queryKey, ...filters }, updater, options);
queryClient.removeQueries({ queryKey, ...filters });
queryClient.cancelQueries({ queryKey, ...filters }, options);
queryClient.invalidateQueries({ queryKey, ...filters }, options);
```

This approach ensures developers can manage and pass parameters more cleanly,
while maintaining a more manageable codebase with fewer type issues.

## [Callbacks on useQuery and QueryObserver have been removed](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#callbacks-on-usequery-and-queryobserver-have-been-removed)

A significant change in TanStack Query v5 is the removal of callbacks such as
`onError`, `onSuccess`, and `onSettled` from `useQuery` and `QueryObserver`.
This change was made to avoid potential misconceptions about their behavior and
to ensure more predictable and consistent side effects.

Previously, we could define `onError` directly within the `useQuery` hook to
handle side effects, such as showing error messages. This eliminated the need
for a separate `useEffect`.

```javascript
const useUsers = () => {
  return useQuery({
    queryKey: ["users", "list"],
    queryFn: fetchUsers,
    onError: error => {
      toast.error(error.message);
    },
  });
};
```

With the removal of the `onError` callback, we now need to handle side effects
using React’s `useEffect`.

```javascript
const useUsers = () => {
  const query = useQuery({
    queryKey: ["users", "list"],
    queryFn: fetchUsers,
  });

  React.useEffect(() => {
    if (query.error) {
      toast.error(query.error.message);
    }
  }, [query.error]);

  return query;
};
```

By using `useEffect`, the issue with this approach becomes much more apparent.
For instance, if `useUsers()` is called twice within the application, it will
trigger two separate error notifications. This is clear when inspecting the
`useEffect` implementation, as each component calling the custom hook registers
an independent effect. In contrast, with the `onError` callback, the behavior
may not be as clear. We might expect errors to be combined, but they are not.

For these types of scenarios, we can use the global callbacks on the
`queryCache`. These global callbacks will run only once for each query and
cannot be overwritten, making them exactly what we need for more predictable
side effect handling.

```javascript
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: error => toast.error(`Something went wrong: ${error.message}`),
  }),
});
```

Another common use case for callbacks was updating local state based on query
data. While using callbacks for state updates can be straightforward, it may
lead to unnecessary re-renders and intermediate render cycles with incorrect
values.

For example, consider the scenario where a query fetches a list of 3 users and
updates the local state with the fetched data.

```javascript
export const useUsers = () => {
  const [usersCount, setUsersCount] = React.useState(0);

  const { data } = useQuery({
    queryKey: ["users", "list"],
    queryFn: fetchUsers,
    onSuccess: data => {
      setUsersCount(data.length);
    },
  });

  return { data, usersCount };
};
```

This example involves three render cycles:

1. Initial Render: The `data` is undefined and `usersCount` is 0 while the query
   is fetching, which is the correct initial state.
2. After Query Resolution: Once the query resolves and `onSuccess` runs, data
   will be an array of 3 users. However, since `setUsersCount` is asynchronous,
   `usersCount` will remain 0 until the state update completes. This is wrong
   because values are not in-sync.
3. Final Render: After the state update completes, `usersCount` is updated to
   reflect the number of users (3), triggering a re-render. At this point, both
   `data` and `usersCount` are in sync and display the correct values.

## [Updated the behavior of refetchInterval callback function](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#the-refetchinterval-callback-function-only-gets-query-passed)

The `refetchInterval` callback now only receives the `query` object as its
argument, instead of both `data` and `query` as it did before. This change
simplifies how callbacks are invoked and it resolves some typing issues that
arose when callbacks were receiving data transformed by the `select` option.

To access the data within the query object, we can now use `query.state.data`.
However, keep in mind that this will not include any transformations applied by
the select option. If we need to access the transformed data, we'll need to
manually reapply the transformation.

For example, consider the following code snippet:

```javascript
const useUsers = () => {
  return useQuery({
    queryKey: ["users", "list"],
    queryFn: fetchUsers,
    select: data => data.users,
    refetchInterval: (data, query) => {
      if (data?.length > 0) {
        return 1000 * 60; // Refetch every minute if there is data
      }
      return false; // Don't refetch if there is no data
    },
  });
};
```

This can now be refactored as follows:

```javascript
const useUsers = () => {
  return useQuery({
    queryKey: ["users", "list"],
    queryFn: fetchUsers,
    select: data => data.users,
    refetchInterval: query => {
      if (query.state.data?.users?.length > 0) {
        return 1000 * 60; // Refetch every minute if there is data
      }
      return false; // Don't refetch if there is no data
    },
  });
};
```

Similarly, the `refetchOnWindowFocus`, `refetchOnMount`, and
`refetchOnReconnect` callbacks now only receive the `query` as an argument.

Below are the changes to the type signature for the `refetchInterval` callback
function:

```javascript
  // before
  refetchInterval: number | false | ((data: TData | undefined, query: Query)
    => number | false | undefined)

  // after
  refetchInterval: number | false | ((query: Query) => number | false | undefined)
```

## [Renamed cacheTime to gcTime](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#renamed-cachetime-to-gctime)

The term `cacheTime` is often misunderstood as the duration for which data is
cached. However, it actually defines how long data remains in the cache after a
query becomes unused. During this period, the data remains active and
accessible. Once the query is no longer in use and the specified `cacheTime`
elapses, the data is considered for "garbage collection" to prevent the cache
from growing excessively. Therefore, the term `gcTime` more accurately describes
this behavior.

```javascript
  const MINUTE = 1000 * 60;

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
  -      // cacheTime: 10 * MINUTE, // before
  +      gcTime: 10 * MINUTE, // after
      },
    },
  })
```

## [Removed keepPreviousData option in favor of placeholderData](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function)

The `keepPreviousData` option and the `isPreviousData` flag have been removed in
TanStack Query v5, as their functionality was largely redundant with the
`placeholderData` and `isPlaceholderData` options.

To replicate the behavior of `keepPreviousData`, the previous query data is now
passed as a parameter to the `placeholderData` option. This option can accept an
identity function to return the previous data, effectively mimicking the same
behavior. Additionally, TanStack Query provides a built-in utility function,
`keepPreviousData`, which can be used directly with `placeholderData` to achieve
the same effect as in previous versions.

Here’s how we can use `placeholderData` to replicate the functionality of
`keepPreviousData`:

```javascript
  import {
    useQuery,
  +  keepPreviousData // Built-in utility function
  } from "@tanstack/react-query";

  const {
    data,
  -  // isPreviousData,
  +  isPlaceholderData, // New
  } = useQuery({
    queryKey,
    queryFn,
  - // keepPreviousData: true,
  + placeholderData: keepPreviousData // New
  });
```

## [Infinite queries now need an initialPageParam](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#infinite-queries-now-need-a-initialpageparam)

In previous versions of TanStack Query, **`undefined`** was passed as the
default page parameter to the query function in infinite queries. This led to
potential issues with non-serializable `undefined` data being stored in the
query cache.

To resolve this, TanStack Query v5 introduces an explicit `initialPageParam`
parameter in the infinite query options. This ensures that the page parameter is
always defined, preventing caching issues and making the query state more
predictable.

```javascript
  useInfiniteQuery({
    queryKey,
  -  // queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam),
    queryFn: ({ pageParam }) => fetchSomething(pageParam),
  +  initialPageParam: 0, // New
    getNextPageParam: (lastPage) => lastPage.next,
  })
```

## [Status and flag updates](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#status-loading-has-been-changed-to-status-pending-and-isloading-has-been-changed-to-ispending-and-isinitialloading-has-now-been-renamed-to-isloading)

The `loading` status is now called `pending`, and the `isLoading` flag has been
renamed to `isPending`. This change also applies to mutations.

Additionally, a new `isLoading` flag has been added for queries. It is now
defined as the logical AND of `isPending` and
`isFetching`(`isPending && isFetching`). This means that `isLoading` behaves the
same as the previous `isInitialLoading`. However, since `isInitialLoading` is
being phased out, it will be removed in the next major version.

## Links

- [Human page](https://www.bigbinary.com/blog/migrating-to-tanstack-query-v5)
