queryClient.getQueryData
现在仅接受 queryKey
作为参数queryClient.getQueryState
现在仅接受 queryKey
作为参数useQuery
(及 QueryObserver
)中的回调函数refetchInterval
回调函数现在仅接收 query
参数useQuery
中移除了 remove
方法useQuery
中移除了 isDataEqual
选项cacheTime
重命名为 gcTime
useErrorBoundary
选项已重命名为 throwOnError
Error
现在是错误的默认类型而非 unknown
prefer-query-object-syntax
规则placeholderData
恒等函数替代 keepPreviousData
focus
事件navigator.onLine
属性context
属性,改用自定义 queryClient
实例refetchPage
,改用 maxPages
dehydrate
APIinitialPageParam
getNextPageParam
或 getPreviousPageParam
返回 null
现在表示没有更多可用页面v5 是一个主要版本,因此需要注意以下重大变更:
useQuery 及相关函数在 TypeScript 中曾有多种重载形式——即函数可以通过不同方式调用。这不仅在类型维护上困难,还需要运行时检查第一和第二参数的类型以正确创建选项。
现在仅支持对象格式:
useQuery(key, fn, options) // [!code --]
useQuery({ queryKey, queryFn, ...options }) // [!code ++]
useInfiniteQuery(key, fn, options) // [!code --]
useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
useMutation(fn, options) // [!code --]
useMutation({ mutationFn, ...options }) // [!code ++]
useIsFetching(key, filters) // [!code --]
useIsFetching({ queryKey, ...filters }) // [!code ++]
useIsMutating(key, filters) // [!code --]
useIsMutating({ mutationKey, ...filters }) // [!code ++]
useQuery(key, fn, options) // [!code --]
useQuery({ queryKey, queryFn, ...options }) // [!code ++]
useInfiniteQuery(key, fn, options) // [!code --]
useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
useMutation(fn, options) // [!code --]
useMutation({ mutationFn, ...options }) // [!code ++]
useIsFetching(key, filters) // [!code --]
useIsFetching({ queryKey, ...filters }) // [!code ++]
useIsMutating(key, filters) // [!code --]
useIsMutating({ mutationKey, ...filters }) // [!code ++]
queryClient.isFetching(key, filters) // [!code --]
queryClient.isFetching({ queryKey, ...filters }) // [!code ++]
queryClient.ensureQueryData(key, filters) // [!code --]
queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++]
queryClient.getQueriesData(key, filters) // [!code --]
queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++]
queryClient.setQueriesData(key, updater, filters, options) // [!code --]
queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++]
queryClient.removeQueries(key, filters) // [!code --]
queryClient.removeQueries({ queryKey, ...filters }) // [!code ++]
queryClient.resetQueries(key, filters, options) // [!code --]
queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.cancelQueries(key, filters, options) // [!code --]
queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.invalidateQueries(key, filters, options) // [!code --]
queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.refetchQueries(key, filters, options) // [!code --]
queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.fetchQuery(key, fn, options) // [!code --]
queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchQuery(key, fn, options) // [!code --]
queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.fetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.isFetching(key, filters) // [!code --]
queryClient.isFetching({ queryKey, ...filters }) // [!code ++]
queryClient.ensureQueryData(key, filters) // [!code --]
queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++]
queryClient.getQueriesData(key, filters) // [!code --]
queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++]
queryClient.setQueriesData(key, updater, filters, options) // [!code --]
queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++]
queryClient.removeQueries(key, filters) // [!code --]
queryClient.removeQueries({ queryKey, ...filters }) // [!code ++]
queryClient.resetQueries(key, filters, options) // [!code --]
queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.cancelQueries(key, filters, options) // [!code --]
queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.invalidateQueries(key, filters, options) // [!code --]
queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.refetchQueries(key, filters, options) // [!code --]
queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.fetchQuery(key, fn, options) // [!code --]
queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchQuery(key, fn, options) // [!code --]
queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.fetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryCache.find(key, filters) // [!code --]
queryCache.find({ queryKey, ...filters }) // [!code ++]
queryCache.findAll(key, filters) // [!code --]
queryCache.findAll({ queryKey, ...filters }) // [!code ++]
queryCache.find(key, filters) // [!code --]
queryCache.find({ queryKey, ...filters }) // [!code ++]
queryCache.findAll(key, filters) // [!code --]
queryCache.findAll({ queryKey, ...filters }) // [!code ++]
queryClient.getQueryData 的参数改为仅接受 queryKey:
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]
queryClient.getQueryState 的参数改为仅接受 queryKey:
queryClient.getQueryState(queryKey, filters) // [!code --]
queryClient.getQueryState(queryKey) // [!code ++]
queryClient.getQueryState(queryKey, filters) // [!code --]
queryClient.getQueryState(queryKey) // [!code ++]
为简化重载移除的迁移工作,v5 提供了代码迁移工具。
该工具会尽力协助迁移重大变更,但请仔细检查生成的代码!此外,某些边缘情况可能无法被工具识别,请留意日志输出。
如需针对 .js 或 .jsx 文件运行,请使用以下命令:
npx jscodeshift@latest ./path/to/src/ \
--extensions=js,jsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
npx jscodeshift@latest ./path/to/src/ \
--extensions=js,jsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
如需针对 .ts 或 .tsx 文件运行,请使用以下命令:
npx jscodeshift@latest ./path/to/src/ \
--extensions=ts,tsx \
--parser=tsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
npx jscodeshift@latest ./path/to/src/ \
--extensions=ts,tsx \
--parser=tsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
注意:对于 TypeScript 文件,必须使用 tsx 作为解析器,否则代码迁移工具可能无法正确应用!
注意: 应用代码迁移工具可能会破坏代码格式,完成后请务必运行 prettier 和/或 eslint!
关于代码迁移工具工作原理的说明:
onSuccess、onError 和 onSettled 已从查询中移除(突变中仍保留)。请参阅 此 RFC 了解变更动机及替代方案。
这统一了回调调用方式(refetchOnWindowFocus、refetchOnMount 和 refetchOnReconnect 回调也仅接收查询参数),并修复了当回调获取通过 select 转换的数据时的一些类型问题。
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --]
+ refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++]
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --]
+ refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++]
仍可通过 query.state.data 访问数据,但不会是通过 select 转换后的数据。如需访问转换后的数据,可对 query.state.data 再次调用转换函数。
此前,remove 方法用于从 queryCache 中移除查询而不通知观察者。通常用于强制移除不再需要的数据,例如用户注销时。
但当查询仍处于活动状态时这样做没有意义,因为下次重新渲染会触发硬加载状态。
如需移除查询,可使用 queryClient.removeQueries({queryKey: key}):
const queryClient = useQueryClient()
const query = useQuery({ queryKey, queryFn })
query.remove() // [!code --]
queryClient.removeQueries({ queryKey }) // [!code ++]
const queryClient = useQueryClient()
const query = useQuery({ queryKey, queryFn })
query.remove() // [!code --]
queryClient.removeQueries({ queryKey }) // [!code ++]
主要因为修复了一个重要的类型推断问题。详情请参阅 TypeScript issue。
此前,该函数用于指示是使用先前的 data(true)还是新数据(false)作为查询的解析数据。
可通过向 structuralSharing 传递函数实现相同功能:
import { replaceEqualDeep } from '@tanstack/react-query'
- isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --]
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++]
import { replaceEqualDeep } from '@tanstack/react-query'
- isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --]
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++]
自定义日志记录器在 v4 中已弃用,在此版本中移除。日志记录仅在开发模式下有效,而在此模式下传递自定义日志记录器并无必要。
我们更新了 browserslist 以生成更现代、性能更好且体积更小的包。要求详见 此处。
TanStack Query 始终在类中有私有字段和方法,但它们并非真正的私有——仅在 TypeScript 中是私有的。现在我们使用 ECMAScript 私有类特性,意味着这些字段在运行时真正私有,无法从外部访问。
几乎每个人都误解了 cacheTime。它听起来像是"数据缓存的时间",但这是错误的。
只要查询仍在使用,cacheTime 就不起作用。仅在查询不再使用时生效。超时后,数据会被"垃圾回收"以避免缓存增长。
gc 指"垃圾回收"时间。这更技术性,但在计算机科学中是 众所周知的缩写。
const MINUTE = 1000 * 60;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- cacheTime: 10 * MINUTE, // [!code --]
+ gcTime: 10 * MINUTE, // [!code ++]
},
},
})
const MINUTE = 1000 * 60;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- cacheTime: 10 * MINUTE, // [!code --]
+ gcTime: 10 * MINUTE, // [!code ++]
},
},
})
为使 useErrorBoundary 选项更框架无关,并避免与 React 钩子的 "use" 前缀和 "ErrorBoundary" 组件名称混淆,现重命名为 throwOnError 以更准确反映其功能。
尽管在 JavaScript 中可以 throw 任何内容(这使得 unknown 是最正确的类型),但几乎总是抛出 Error(或其子类)。此变更使在 TypeScript 中处理 error 字段对大多数情况更简单。
如需抛出非 Error 的内容,现在需自行设置泛型:
useQuery<number, string>({
queryKey: ['some-query'],
queryFn: async () => {
if (Math.random() > 0.5) {
throw 'some error'
}
return 42
},
})
useQuery<number, string>({
queryKey: ['some-query'],
queryFn: async () => {
if (Math.random() > 0.5) {
throw 'some error'
}
return 42
},
})
如需全局设置不同类型的错误,请参阅 TypeScript 指南。
由于现在唯一支持的语法是对象语法,此规则不再需要。
我们移除了 keepPreviousData 选项和 isPreviousData 标志,因为它们的功能与 placeholderData 和 isPlaceholderData 标志基本相同。
为实现与 keepPreviousData 相同的功能,我们向 placeholderData 添加了先前查询 data 作为参数,它接受一个恒等函数。因此只需向 placeholderData 提供恒等函数或使用 Tanstack Query 包含的 keepPreviousData 函数。
注意:useQueries 不会在 placeholderData 函数中接收 previousData 作为参数。这是由于传入数组的查询的动态性质可能导致占位符和 queryFn 的结果形状不同。
import {
useQuery,
+ keepPreviousData // [!code ++]
} from "@tanstack/react-query";
const {
data,
- isPreviousData, // [!code --]
+ isPlaceholderData, // [!code ++]
} = useQuery({
queryKey,
queryFn,
- keepPreviousData: true, // [!code --]
+ placeholderData: keepPreviousData // [!code ++]
});
import {
useQuery,
+ keepPreviousData // [!code ++]
} from "@tanstack/react-query";
const {
data,
- isPreviousData, // [!code --]
+ isPlaceholderData, // [!code ++]
} = useQuery({
queryKey,
queryFn,
- keepPreviousData: true, // [!code --]
+ placeholderData: keepPreviousData // [!code ++]
});
在 Tanstack Query 的上下文中,恒等函数指始终返回其提供的参数(即数据)而不做更改的函数。
useQuery({
queryKey,
queryFn,
placeholderData: (previousData, previousQuery) => previousData, // 与 `keepPreviousData` 行为相同的恒等函数
})
useQuery({
queryKey,
queryFn,
placeholderData: (previousData, previousQuery) => previousData, // 与 `keepPreviousData` 行为相同的恒等函数
})
但此变更有一些注意事项:
placeholderData 会始终进入 success 状态,而 keepPreviousData 给出先前查询的状态。如果数据成功获取后遇到后台刷新错误,状态可能是 error。但由于错误本身未共享,我们决定坚持 placeholderData 的行为。
keepPreviousData 提供先前数据的 dataUpdatedAt 时间戳,而使用 placeholderData 时 dataUpdatedAt 保持为 0。如需在屏幕上连续显示该时间戳可能会不便,但可通过 useEffect 解决:
const [updatedAt, setUpdatedAt] = useState(0)
const { data, dataUpdatedAt } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
})
useEffect(() => {
if (dataUpdatedAt > updatedAt) {
setUpdatedAt(dataUpdatedAt)
}
}, [dataUpdatedAt])
const [updatedAt, setUpdatedAt] = useState(0)
const { data, dataUpdatedAt } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
})
useEffect(() => {
if (dataUpdatedAt > updatedAt) {
setUpdatedAt(dataUpdatedAt)
}
}, [dataUpdatedAt])
现在仅使用 visibilitychange 事件。这是可行的,因为我们仅支持支持 visibilitychange 事件的浏览器。这修复了 此处列出 的一系列问题。
navigator.onLine 在基于 Chromium 的浏览器中效果不佳。存在 许多问题 关于误报,导致查询被错误标记为 offline。
为避免此问题,我们现在始终以 online: true 开始,仅监听 online 和 offline 事件来更新状态。
这应减少误报的可能性,但对于通过 serviceWorkers 加载的离线应用,可能意味着误报,因为这些应用即使没有互联网连接也能工作。
在 v4 中,我们引入了向所有 react-query 钩子传递自定义 context 的可能性。这在使用微前端时实现了适当的隔离。
然而,context 是仅 React 的特性。context 所做的只是让我们访问 queryClient。我们可以通过允许直接传入自定义 queryClient 实现相同的隔离。 这反过来使其他框架能够以框架无关的方式拥有相同的功能。
import { queryClient } from './my-client'
const { data } = useQuery(
{
queryKey: ['users', id],
queryFn: () => fetch(...),
- context: customContext // [!code --]
},
+ queryClient, // [!code ++]
)
import { queryClient } from './my-client'
const { data } = useQuery(
{
queryKey: ['users', id],
queryFn: () => fetch(...),
- context: customContext // [!code --]
},
+ queryClient, // [!code ++]
)
在 v4 中,我们引入了通过 refetchPage 函数定义无限查询中要重新获取的页面的可能性。
然而,重新获取所有页面可能导致 UI 不一致。此外,此选项在例如 queryClient.refetchQueries 上可用,但仅对无限查询有效,而非"普通"查询。
v5 为无限查询引入了新的 maxPages 选项,以限制存储在查询数据中和重新获取的页面数量。此新功能处理了最初为 refetchPage 功能识别的用例,而无需相关的问题。
传递给 dehydrate 的选项已简化。查询和突变始终根据默认函数实现进行脱水。要更改此行为,可以使用移除的布尔选项 dehydrateMutations 和 dehydrateQueries 的函数等效项 shouldDehydrateQuery 或 shouldDehydrateMutation。要完全不水合查询/突变,传入 () => false。
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]
此前,我们向 queryFn 传递 undefined 作为 pageParam,并且可以在 queryFn 函数签名中为 pageParam 参数分配默认值。这导致在 queryCache 中存储不可序列化的 undefined。
现在,必须向无限查询选项显式传递 initialPageParam。这将用作第一页的 pageParam:
useInfiniteQuery({
queryKey,
- queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --]
+ queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++]
+ initialPageParam: 0, // [!code ++]
getNextPageParam: (lastPage) => lastPage.next,
})
useInfiniteQuery({
queryKey,
- queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --]
+ queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++]
+ initialPageParam: 0, // [!code ++]
getNextPageParam: (lastPage) => lastPage.next,
})
此前,我们允许通过直接向 fetchNextPage 或 fetchPreviousPage 传递 pageParam 值来覆盖从 getNextPageParam 或 getPreviousPageParam 返回的 pageParams。此功能在重新获取时完全无效,且不广为人知或使用。这也意味着无限查询现在必须定义 getNextPageParam。
在 v4 中,需要显式返回 undefined 表示没有更多可用页面。我们扩展了此检查以包括 `null