迁移到 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 ++]

代码转换

为了更轻松地迁移 remove 重载,v5 提供了一个 codemod。

代码转换工具是尽最大努力帮助您迁移破坏性更改的尝试。请仔细审查生成的代码!另外,有一些代码转换工具无法发现的边缘情况,因此请留意日志输出。

如果您想针对 .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 作为解析器;否则,codemod 将无法正确应用!

注意:应用 codemod 可能会破坏您的代码格式,因此请在应用 codemod 后不要忘记运行 prettier 和/或 eslint

关于 codemod 如何工作的几点说明

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

useQuery(和 QueryObserver)的回调已删除

onSuccessonErroronSettled 已从 Queries 中移除。它们未被修改以用于 Mutations。有关此变更的动机以及替代方案,请参阅 此 RFC

refetchInterval 回调函数仅接收 query 作为参数

这简化了回调的调用方式(refetchOnWindowFocusrefetchOnMountrefetchOnReconnect 回调也都只将 query 作为参数传递),并且修复了回调获取被 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 再次调用转换函数。

useQuery 中已移除 remove 方法

以前,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 issue 以获取更多信息。

useQuery 中已移除 isDataEqual 选项

以前,此函数用于指示是使用先前的 datatrue)还是新的 datafalse)作为查询的已解析数据。

您可以通过将函数传递给 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.x 版本中已被弃用,并在本版本中被移除。日志记录仅在开发模式下有效,而在开发模式下,无需传递自定义日志记录器。

支持的浏览器

我们更新了 browserslist 以生成更现代、更高效、更小的包。您可以在 此处 阅读有关要求的详细信息。

私有类字段和方法

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

重命名 cacheTimegcTime

几乎所有人都对 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 选项更具框架无关性,并避免与 hook 的已建立的 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
  },
})

有关全局设置不同类型 Error 的方法,请参阅 TypeScript 指南

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

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

移除 keepPreviousData,改用 placeholderData 身份函数

我们已移除 keepPreviousData 选项和 isPreviousData 标志,因为它们与 placeholderDataisPlaceholderData 标志的功能基本相同。

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

这里需要注意的是,useQueriesplaceholderData 函数中不会收到 previousData 作为参数。这是由于传递在数组中的查询的动态性质,这可能会导致 placeholder 和 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, // identity function with the same behaviour as `keepPreviousData`
})
useQuery({
  queryKey,
  queryFn,
  placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `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 开始,并且只监听 onlineoffline 事件来更新状态。

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

移除了自定义 context prop,改用自定义 queryClient 实例

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

然而,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 的选项已简化。Queries 和 Mutations 始终会被脱水(根据默认函数实现)。要更改此行为,而不是使用已移除的布尔选项 dehydrateMutationsdehydrateQueries,您可以实现函数等效的 shouldDehydrateQueryshouldDehydrateMutation。要获得不进行查询/mutation 脱水的旧行为,请传递 () => false

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

无限查询现在需要 initialPageParam

以前,我们将 undefined 传递给 queryFn 作为 pageParam,并且您可以在 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 值直接传递给 fetchNextPagefetchPreviousPage 来覆盖 getNextPageParamgetPreviousPageParam 返回的 pageParams。此功能根本不适用于重试,而且鲜为人知或很少使用。这也意味着 getNextPageParam 现在是无限查询所必需的。

getNextPageParamgetPreviousPageParam 返回 null 现在表示没有更多页面可用

在 v4 中,您需要明确返回 undefined 来表示没有更多页面可用。我们已扩大此检查范围以包括 null

服务器上不重试

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

status: loading 已更改为 status: pendingisLoading 已更改为 isPendingisInitialLoading 已重命名为 isLoading

loading 状态已重命名为 pending,同样,派生的 isLoading 标志已重命名为 isPending

对于 Mutations,status 也已从 loading 更改为 pendingisLoading 标志已更改为 isPending

最后,为查询添加了一个新的派生 isLoading 标志,它实现为 isPending && isFetching。这意味着 isLoadingisInitialLoading 具有相同的功能,但 isInitialLoading 已弃用,将在下一个主版本中移除。

要了解此更改背后的原因,请查看 v5 road map 讨论

hashQueryKey 已重命名为 hashKey

因为它可以对 mutation keys 进行哈希处理,并且可以在 useIsMutatinguseMutationStatepredicate 函数中使用,这些函数会接收 mutations。

Vue Query 重大变更

useQueries composable 返回 ref 而不是 reactive

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

  • 用户可以展开返回值,从而失去响应性。
  • 返回值使用的 readonly 包装器破坏了 Vue 2 的响应性检测机制。这在 Vue 2.6 中是隐藏的问​​题,但在 Vue 2.7 中表现为错误。
  • Vue 2 不支持数组作为 reactive 的根值。

通过此更改,所有这些问题都已得到修复。

这也使 useQueries 与其他返回所有值为 refs 的 composables 保持一致。

现在需要 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 选项,允许开发人员限制存储在查询数据中并随后重取的页面数量。您可以根据您想要提供的用户体验和重取性能来调整 maxPages 值。

请注意,无限列表必须是双向的,这需要同时定义 getNextPageParamgetPreviousPageParam

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

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

useQueries 的新 combine 选项

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

实验性的 fine grained storage persister

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

能够在 injectionContext 中运行 vue-query composables

以前 vue-query composables 只能在组件的 setup 函数中运行。
我们有一个紧急出口,允许用户在将 queryClient 作为 composable 选项提供的情况下在任何地方运行这些 hook。

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