Skip to main content
React Query v3 brings significant improvements, better SSR support, and many new features while refining the API based on community feedback.

Overview of Changes

Architecture

Scalable and testable cache configuration with separated QueryCache and MutationCache

SSR Support

Improved server-side rendering capabilities

Data Lag

Keep previous data visible while new data loads (replaces usePaginatedQuery)

Bi-directional

Infinite queries can now paginate in both directions

Selectors

Query data selectors for transformation and memoization

useQueries

New hook for variable-length parallel query execution

Breaking Changes

QueryCache Split into QueryClient and Caches

The QueryCache has been split into QueryClient, QueryCache, and MutationCache:
import { QueryCache } from 'react-query'

const queryCache = new QueryCache()

queryCache.prefetchQuery('todos', fetchTodos)
Benefits:
  • Different types of caches
  • Multiple clients with different configurations can share the same cache
  • Cleaner API focused on general usage
  • Easier to test individual components

New QueryClientProvider

import { ReactQueryCacheProvider, ReactQueryConfigProvider } from 'react-query'

const queryCache = new QueryCache()

function App() {
  return (
    <ReactQueryConfigProvider config={{ queries: { staleTime: 60000 } }}>
      <ReactQueryCacheProvider queryCache={queryCache}>
        <YourApp />
      </ReactQueryCacheProvider>
    </ReactQueryConfigProvider>
  )
}
Note the change from defaultConfig to defaultOptions.

Default QueryCache Removed

import { useQuery } from 'react-query'

// Uses default global cache
function Component() {
  const { data } = useQuery('todos', fetchTodos)
}

prefetchQuery Changed

const data = await queryCache.prefetchQuery('todos', fetchTodos)

Error Boundaries

import { ReactQueryErrorResetBoundary } from 'react-query'

<ReactQueryErrorResetBoundary>
  {({ reset }) => (
    <ErrorBoundary onReset={reset}>
      <App />
    </ErrorBoundary>
  )}
</ReactQueryErrorResetBoundary>

Cache Methods Renamed

queryCache.getQuery('todos')
queryCache.getQueries(['todos'])

isFetching is Now a Method

const isFetching = queryCache.isFetching

useQueryCacheuseQueryClient

import { useQueryCache } from 'react-query'

const queryCache = useQueryCache()

Query Functions No Longer Receive Split Parameters

useQuery(['post', id], (_key, id) => fetchPost(id))

Infinite Query Page Params

useInfiniteQuery(
  ['posts'],
  (_key, pageParam = 0) => fetchPosts(pageParam)
)

usePaginatedQuery Removed

Use keepPreviousData option instead:
import { usePaginatedQuery } from 'react-query'

const { data, resolvedData } = usePaginatedQuery(['page', page], fetchPage)

Infinite Queries are Bi-directional

const {
  data,
  fetchMore,
  canFetchMore,
  isFetchingMore,
} = useInfiniteQuery('projects', fetchProjects, {
  getFetchMore: (lastPage) => lastPage.nextCursor,
})

if (data) {
  return data.map((page) => page.data)
}
Changes:
  • getFetchMoregetNextPageParam
  • canFetchMorehasNextPage
  • fetchMorefetchNextPage
  • isFetchingMoreisFetchingNextPage
  • Added getPreviousPageParam, hasPreviousPage, fetchPreviousPage, isFetchingPreviousPage
  • Data is now { pages: [...], pageParams: [...] }

useMutation Returns Object

const [mutate, { status, reset }] = useMutation(addTodo)

mutate No Longer Returns Promise

const [mutate] = useMutation(addTodo)

try {
  const data = await mutate('todo')
  console.log(data)
} catch (error) {
  console.error(error)
}

Query Options Collapsed

useQuery({
  queryKey: 'posts',
  queryFn: fetchPosts,
  config: { staleTime: Infinity },
})

enabled Must Be Boolean

useQuery('user', fetchUser, {
  enabled: userId, // truthy/falsy
})

initialStale Option Removed

Initial data now respects staleTime:
useQuery('todos', fetchTodos, {
  initialData: cachedTodos,
  initialStale: true,
})

refetchOnMount Scoped to Component

// refetchOnMount: false affected all observers
useQuery('todos', fetchTodos, { refetchOnMount: false })

notifyOnStatusChange Replaced

useQuery('todos', fetchTodos, {
  notifyOnStatusChange: false,
})

clear() Renamed to remove()

const query = useQuery('todos', fetchTodos)
query.clear()

updatedAt Split

const { updatedAt } = useQuery('todos', fetchTodos)

setConsole() Replaced

import { setConsole } from 'react-query'

setConsole({ log, warn, error })

React Native Auto-Configuration

React Native error logging is now automatic (no need to override console).

TypeScript: QueryStatus is Union Type

import { useQuery, QueryStatus } from 'react-query'

const { status } = useQuery('todos', fetchTodos)

if (status === QueryStatus.Loading) {
  return <Spinner />
}
Enum values to string literals:
  • QueryStatus.Idle'idle'
  • QueryStatus.Loading'loading'
  • QueryStatus.Error'error'
  • QueryStatus.Success'success'

New Features

Query Data Selectors

function User() {
  const { data } = useQuery('user', fetchUser, {
    select: (user) => user.username,
  })
  
  return <div>Username: {data}</div>
}
Combine with notifyOnChangeProps for optimal performance:
useQuery('user', fetchUser, {
  select: (user) => user.username,
  notifyOnChangeProps: ['data', 'error'],
})

useQueries Hook

const results = useQueries([
  { queryKey: ['post', 1], queryFn: fetchPost },
  { queryKey: ['post', 2], queryFn: fetchPost },
])

return (
  <ul>
    {results.map(({ data }) => (
      data && <li key={data.id}>{data.title}</li>
    ))}
  </ul>
)

Mutation Retry & Offline Support

const mutation = useMutation(addTodo, {
  retry: 3,
})

// Mutations retry when device comes back online

Persist Mutations

Mutations can be persisted and resumed:
import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental'

const persister = createWebStoragePersistor({ storage: window.localStorage })

persistQueryClient({
  queryClient,
  persister,
})

Query Observers

Watch queries outside React:
const observer = new QueryObserver(queryClient, { queryKey: 'posts' })

const unsubscribe = observer.subscribe((result) => {
  console.log(result)
})
Also available:
  • InfiniteQueryObserver
  • QueriesObserver
  • MutationObserver

Pre-Configure Queries

queryClient.setQueryDefaults('posts', { queryFn: fetchPosts })

function Component() {
  const { data } = useQuery('posts')
}

Pre-Configure Mutations

queryClient.setMutationDefaults('addPost', { mutationFn: addPost })

function Component() {
  const { mutate } = useMutation({ mutationKey: 'addPost' })
}

useIsFetching with Filters

const isFetchingPosts = useIsFetching(['posts'])

if (isFetchingPosts) {
  return <Spinner />
}

Core Separation

Use React Query core without React:
import { QueryClient } from 'react-query/core'

const queryClient = new QueryClient()

Devtools in Main Package

import { ReactQueryDevtools } from 'react-query-devtools'

Migration Checklist

1

Update QueryCache to QueryClient

Replace all QueryCache instantiation with QueryClient.
2

Update Providers

Replace ReactQueryCacheProvider and ReactQueryConfigProvider with QueryClientProvider.
3

Update Hook Names

  • useQueryCacheuseQueryClient
  • usePaginatedQueryuseQuery with keepPreviousData
4

Update Infinite Queries

Rename properties and update to use pages data structure.
5

Update Mutations

Change from array to object destructuring.
6

Update Query Function Signatures

Use inline functions or QueryFunctionContext.
7

Update Options

Flatten config into main options object.
8

Update Method Names

  • getQueryfind
  • getQueriesfindAll
  • clearremove
9

Test Everything

Thoroughly test all queries, mutations, and cache interactions.
v3 represents a major evolution of React Query with better architecture, improved TypeScript support, and many powerful new features.

Build docs developers (and LLMs) love