迁移到 TanStack Query v5

重大更改

v5 是一个主要版本,因此需要注意一些重大更改:

支持单一签名,一个对象

useQuery 及其相关函数以前在 TypeScript 中有许多重载:可以调用该函数的不同方式。这不仅在类型方面难以维护,而且还需要在运行时检查第一个和第二个参数的类型,以正确创建选项。

现在我们只支持对象格式。

tsx
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 ++]
tsx
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 ++]
tsx
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

tsx
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]

queryClient.getQueryState 现在仅接受 queryKey 作为参数

queryClient.getQueryState 参数已更改为仅接受 queryKey

tsx
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!

关于代码转换工具如何工作的一些说明:

  • 通常,我们正在寻找幸运的情况,即第一个参数是一个对象表达式并且包含“queryKey”或“mutationKey”属性(取决于正在转换的钩子/方法调用)。如果是这种情况,您的代码已经匹配新的签名,因此代码转换工具不会修改它。🎉
  • 如果不满足上述条件,则代码转换工具将检查第一个参数是否是数组表达式或引用数组表达式的标识符。如果是这种情况,代码转换工具会将其放入一个对象表达式中,然后它将成为第一个参数。
  • 如果可以推断出对象参数,代码转换工具将尝试将已存在的属性复制到新创建的属性中。
  • 如果代码转换工具无法推断出用法,则它将在控制台上留下一条消息。该消息包含文件名和用法的行号。在这种情况下,您需要手动进行迁移。
  • 如果转换导致错误,您还将在控制台上看到一条消息。此消息将通知您发生了意外情况,请手动进行迁移。

useQuery(和 QueryObserver)上的回调已被删除

onSuccess、onError 和 onSettled 已从查询中删除。它们未对变更进行修改。有关此更改背后的动机以及应采取的措施,请参阅此 RFC。

refetchInterval 回调函数仅传递 query

这简化了回调的调用方式(refetchOnWindowFocus、refetchOnMount 和 refetchOnReconnect 回调也都只传递查询),并且修复了一些当回调获取由 select 转换的数据时的类型问题。

tsx
- 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 方法已从 useQuery 中删除

以前,remove 方法用于从 queryCache 中删除查询,而不会通知观察者。它最适合用于命令式地删除不再需要的数据,例如,在注销用户时。

但是,当查询仍处于活动状态时执行此操作没有多大意义,因为它只会在下一次重新渲染时触发硬加载状态。

如果您仍然需要删除查询,可以使用 queryClient.removeQueries({queryKey: key})

tsx
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 版本现在是 4.7

主要是因为围绕类型推断发布了一个重要的修复程序。有关更多信息,请参阅此 TypeScript 问题。

isDataEqual 选项已从 useQuery 中删除

以前,此函数用于指示是将先前 data (true) 还是新数据 (false) 用作查询的已解析数据。

您可以通过向 structuralSharing 传递一个函数来实现相同的功能:

tsx
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 ++]

已弃用的自定义记录器已被删除

自定义记录器在版本 4 中已被弃用,并在此版本中被删除。日志记录仅在开发模式下有效,在这种模式下不需要传递自定义记录器。

支持的浏览器

我们更新了我们的 browserslist 以生成更现代、性能更好且更小的捆绑包。您可以在此处阅读有关要求的信息。

私有类字段和方法

TanStack Query 始终在类上具有私有字段和方法,但它们并非真正的私有——它们只是在 TypeScript 中是私有的。我们现在使用 ECMAScript 私有类特性,这意味着这些字段现在是真正的私有字段,并且无法在运行时从外部访问。

将 cacheTime 重命名为 gcTime

几乎每个人都弄错了 cacheTime。它听起来像是“数据缓存的时间量”,但这是不正确的。

只要查询仍在使用中,cacheTime 就不会执行任何操作。它仅在查询变为未使用状态后才会生效。时间过后,数据将被“垃圾回收”以避免缓存增长。

gc 指的是“垃圾回收”时间。它有点技术性,但在计算机科学中也是一个相当众所周知的缩写。

tsx
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 选项已重命名为 throwOnError

为了使 useErrorBoundary 选项更具框架无关性,并避免与已建立的 React 函数前缀“use”(用于钩子)和“ErrorBoundary”组件名称混淆,它已重命名为 throwOnError 以更准确地反映其功能。

TypeScript:Error 现在是错误的默认类型,而不是 unknown

尽管在 JavaScript 中,您可以 throw 任何东西(这使得 unknown 成为最正确的类型),但几乎总是抛出 Errors(或 Error 的子类)。此更改使得在大多数情况下更容易在 TypeScript 中使用 error 字段。

如果您想抛出非 Error 的东西,现在必须自己设置泛型:

ts
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 指南。

eslint prefer-query-object-syntax 规则已删除

由于现在唯一支持的语法是对象语法,因此不再需要此规则

删除 keepPreviousData,取而代之的是 placeholderData 标识函数

我们删除了 keepPreviousData 选项和 isPreviousData 标志,因为它们的功能与 placeholderData 和 isPlaceholderData 标志基本相同。

为了实现与 keepPreviousData 相同的功能,我们将先前的查询 data 作为参数添加到了 placeholderData 中,该参数接受一个标识函数。因此,您只需要向 placeholderData 提供一个标识函数,或者使用 Tanstack Query 中包含的 keepPreviousData 函数。

此处需要注意的一点是,useQueries 不会在 placeholderData 函数中接收 previousData 作为参数。这是由于数组中传递的查询的动态特性,这可能导致占位符和 queryFn 的结果形状不同。

tsx
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 的上下文中,标识函数是指始终返回其提供的参数(即数据)不变的函数。

ts
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 来解决此问题。

    ts
    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])
    

窗口焦点重新获取不再侦听 focus 事件

现在仅使用 visibilitychange 事件。这是可能的,因为我们仅支持支持 visibilitychange 事件的浏览器。这修复了此处列出的许多问题。

网络状态不再依赖于 navigator.onLine 属性

navigator.onLine 在基于 Chromium 的浏览器中效果不佳。存在许多问题围绕假阴性,这导致查询被错误地标记为 offline。

为了规避这个问题,我们现在总是以 online: true 开始,并且只侦听 online 和 offline 事件来更新状态。

这应该会降低假阴性的可能性,但是,对于通过 serviceWorker 加载的离线应用程序(即使没有互联网连接也可以工作),这可能意味着假阳性。

删除自定义 context 属性,取而代之的是自定义 queryClient 实例

在 v4 中,我们引入了将自定义 context 传递给所有 react-query 钩子的可能性。这允许在使用 MicroFrontends 时进行适当的隔离。

但是,context 是一个仅限 react 的功能。context 所做的只是让我们能够访问 queryClient。我们可以通过允许直接传入自定义 queryClient 来实现相同的隔离。 这反过来将使其他框架能够以与框架无关的方式拥有相同的功能。

tsx
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 ++]
)

删除 refetchPage,取而代之的是 maxPages

在 v4 中,我们引入了使用 refetchPage 函数为无限查询定义要重新获取的页面的可能性。

但是,重新获取所有页面可能会导致 UI 不一致。此外,此选项在例如 queryClient.refetchQueries 上可用,但它仅对无限查询有效,对“普通”查询无效。

v5 包含一个新的 maxPages 选项,用于无限查询,以限制存储在查询数据中并重新获取的页面数量。此新功能处理了最初为 refetchPage 页面功能确定的用例,而没有相关问题。

新的 dehydrate API

您可以传递给 dehydrate 的选项已简化。查询和变更始终根据默认函数实现进行脱水。要更改此行为,请不要使用已删除的布尔选项 dehydrateMutations 和 dehydrateQueries,而是实现等效的函数 shouldDehydrateQuery 或 shouldDehydrateMutation。要获得完全不水合查询/变更的旧行为,请传入 () => false。

tsx
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]

无限查询现在需要 initialPageParam

以前,我们将 undefined 作为 pageParam 传递给 queryFn,您可以在 queryFn 函数签名中为 pageParam 参数分配默认值。这样做的缺点是在 queryCache 中存储了 undefined,这是不可序列化的。

相反,您现在必须将显式的 initialPageParam 传递给无限查询选项。这将用作第一页的 pageParam:

tsx
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,
})

无限查询的手动模式已被删除

以前,我���允许通过将 pageParam 值直接传递给 fetchNextPage 或 fetchPreviousPage 来覆盖从 getNextPageParam 或 getPreviousPageParam 返回的 pageParams。此功能根本无法与重新获取一起使用,并且未被广泛了解或使用。这也意味着 getNextPageParam 现在是无限查询所必需的。

从 getNextPageParam 或 getPreviousPageParam 返回 null 现在表示没有更多可用页面

在 v4 中,您需要显式返回 undefined 以指示没有更多可用页面。我们已将此检查范围扩大到包括 null。

服务器上没有重试

在服务器上,retry 现在默认为 0 而不是 3。对于预取,我们始终默认为 0 次重试,但由于启用了 suspense 的查询现在也可以直接在服务器上执行(从 React18 开始),因此我们必须确保根本不在服务器上重试。

status: loading 已更改为 status: pending,isLoading 已更改为 isPending,isInitialLoading 现已重命名为 isLoading

loading 状态已重命名为 pending,类似地,派生的 isLoading 标志已重命名为 isPending。

对于变更,status 也从 loading 更改为 pending,isLoading 标志已更改为 isPending。

最后,向查询中添加了一个新的派生 isLoading 标志,其实现为 isPending && isFetching。这意味着 isLoading 和 isInitialLoading 是相同的东西,但 isInitialLoading 现在已被弃用,并将在下一个主要版本中删除。

要了解此更改背后的原因,请查看 v5 路线图讨论。

hashQueryKey 已重命名为 hashKey

因为它还会对变更键进行哈希处理,并且可以在 useIsMutating 和 useMutationState 的 predicate 函数内部使用,这些函数会传递变更。

Vue Query 重大更改

useQueries 组合式函数返回 ref 而不是 reactive

为了修复与 Vue 2 的兼容性,useQueries 组合式函数现在返回包装在 ref 中的 queries 数组。 以前返回的是 reactive,这导致了多个问题:

  • 用户可能会传播返回值从而丢失响应性。
  • 用于返回值的 readonly 包装器破坏了 Vue 2 的响应性检测机制。这在 Vue 2.6 中是一个静默问题,但在 Vue 2.7 中显示为错误。
  • Vue 2 不支持数组作为 reactive 的根值。

通过此更改,所有这些问题都得到了解决。

此外,这使 useQueries 与其他返回所有值作为 refs 的组合式函数保持一致。

现在需要 Vue v3.3

为了能够提供遵循 Vue 版本的新功能,我们现在要求 Vue 3 至少为 v3.3 版本。 对 Vue 2.x 的要求保持不变。

新功能 🚀

v5 还带来了新功能:

简化的乐观更新

我们有一种新的、简化的方式来执行乐观更新,方法是利用从 useMutation 返回的 variables:

tsx
const queryInfo = useTodos()
const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

if (queryInfo.data) {
  return (
    <ul>
      {queryInfo.data.items.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
      {addTodoMutation.isPending && (
        <li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
          {addTodoMutation.variables}
        </li>
      )}
    </ul>
  )
}
const queryInfo = useTodos()
const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

if (queryInfo.data) {
  return (
    <ul>
      {queryInfo.data.items.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
      {addTodoMutation.isPending && (
        <li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
          {addTodoMutation.variables}
        </li>
      )}
    </ul>
  )
}

在这里,我们仅在变更运行时更改 UI 的外观,而不是直接将数据写入缓存。如果只有一个地方需要显示乐观更新,则此方法效果最佳。有关更多详细信息,请查看乐观更新文档。

受限的无限查询与新的 maxPages 选项

无限查询在需要无限滚动或分页时非常有用。 但是,获取的页面越多,消耗的内存就越多,这也会减慢查询重新获取过程,因为所有页面都是按顺序重新获取的。

版本 5 为无限查询提供了一个新的 maxPages 选项,允许开发人员限制存储在查询数据中并随后重新获取的页面数量。 您可以根据要提供的 UX 和重新获取性能调整 maxPages 值。

请注意,无限列表必须是双向的,这要求同时定义 getNextPageParam 和 getPreviousPageParam。

无限查询可以预取多个页面

无限查询可以像常规查询一样进行预取。默认情况下,仅预取查询的第一页,并将其存储在给定的 QueryKey 下。如果要预取多个页面,可以使用 pages 选项。有关更多信息,请阅读预取指南。

useQueries 的新 combine 选项

有关更多详细信息,请参阅 useQueries 文档。

实验性的 细粒度存储持久化程序

有关更多详细信息,请参阅 experimental_createPersister 文档。

能够在 injectionContext 中运行 vue-query 组合式函数

以前,vue-query 组合式函数只能在组件的 setup 函数中运行。 我们有一个应急方案,允许在用户将 queryClient 作为组合式函数选项提供的情况下在任何地方运行这些钩子。

现在,您可以在任何支持 injectionContext 的函数中使用 vue-query 组合式函数。例如,路由器导航守卫。 使用此新功能时,请确保 vue-query 组合式函数在 effectScope 内运行。否则,可能会导致内存泄漏。 我们添加了 dev-only 警告以通知用户潜在的误用。