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
useQueryCache → useQueryClient
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:
getFetchMore → getNextPageParam
canFetchMore → hasNextPage
fetchMore → fetchNextPage
isFetchingMore → isFetchingNextPage
- 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
queryClient.setQueryDefaults('posts', { queryFn: fetchPosts })
function Component() {
const { data } = useQuery('posts')
}
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
Update QueryCache to QueryClient
Replace all QueryCache instantiation with QueryClient.
Update Providers
Replace ReactQueryCacheProvider and ReactQueryConfigProvider with QueryClientProvider.
Update Hook Names
useQueryCache → useQueryClient
usePaginatedQuery → useQuery with keepPreviousData
Update Infinite Queries
Rename properties and update to use pages data structure.
Update Mutations
Change from array to object destructuring.
Update Query Function Signatures
Use inline functions or QueryFunctionContext.
Update Options
Flatten config into main options object.
Update Method Names
getQuery → find
getQueries → findAll
clear → remove
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.