框架
版本

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

Codemod

为了使 remove 重载迁移更容易,v5 附带了一个 codemod。

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" 属性(取决于正在转换的钩子/方法调用)。如果是这种情况,您的代码已经与新签名匹配,因此 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 上调用转换。

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

已弃用的自定义 logger 已被移除

自定义记录器在版本 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
  },
})

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

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

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

移除 keepPreviousData,改用 placeholderData 恒等函数

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

为了实现与 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, // 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 的浏览器中效果不佳。关于误报有很多问题,这导致 Queries 被错误地标记为 offline

为了规避此问题,我们现在始终以 online: true 开头,并且仅监听 onlineoffline 事件以更新状态。

这应减少误报的可能性,但是,这可能意味着通过 serviceWorkers 加载的离线应用程序的误报,即使没有互联网连接,这些应用程序也可以工作。

移除自定义 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 的选项已得到简化。查询和突变始终是脱水的(根据默认函数实现)。要更改此行为,而不是使用已移除的布尔选项 dehydrateMutationsdehydrateQueries,您可以实现函数等效项 shouldDehydrateQueryshouldDehydrateMutation。要获得完全不水合查询/突变的旧行为,请传入 () => false

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

无限查询现在需要 initialPageParam

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

相反,您现在必须将显式的 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

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

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

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

hashQueryKey 已重命名为 hashKey

因为它也哈希突变键,并且可以在 useIsMutatinguseMutationStatepredicate 函数中使用,后者会传递突变。

现在所需的最低 React 版本为 18.0

React Query v5 需要 React 18.0 或更高版本。这是因为我们正在使用新的 useSyncExternalStore hook,它仅在 React 18.0 及更高版本中可用。之前,我们一直使用 React 提供的 shim。

已从 QueryClientProvider 中移除 contextSharing prop

您之前可以使用 contextSharing 属性在窗口之间共享查询客户端上下文的第一个(以及至少一个)实例。这确保了如果 TanStack Query 在不同的 bundle 或微前端中使用,它们都将使用相同的上下文实例,而与模块作用域无关。

随着 v5 中自定义上下文 prop 的移除,请参阅关于 移除自定义上下文 prop 而支持自定义 queryClient 实例 的部分。如果您希望在应用程序的多个包之间共享同一个查询客户端,您可以直接传递一个共享的自定义 queryClient 实例。

不再在 React 和 React Native 中使用 unstable_batchedUpdates 作为批处理函数

由于函数 unstable_batchedUpdates 在 React 18 中是 noop,因此它将不再自动设置为 react-query 中的批处理函数。

如果您的框架支持自定义批处理函数,您可以通过调用 notifyManager.setBatchNotifyFunction 来告知 TanStack Query。

例如,这就是在 solid-query 中设置批处理函数的方式

ts
import { notifyManager } from '@tanstack/query-core'
import { batch } from 'solid-js'

notifyManager.setBatchNotifyFunction(batch)
import { notifyManager } from '@tanstack/query-core'
import { batch } from 'solid-js'

notifyManager.setBatchNotifyFunction(batch)

水合 API 变更

为了更好地支持并发特性和过渡,我们对 hydration API 进行了一些更改。Hydrate 组件已重命名为 HydrationBoundary,并且 useHydrate hook 已被移除。

HydrationBoundary 不再 hydrate mutations,仅 hydrate queries。要 hydrate mutations,请使用底层的 hydrate API 或 persistQueryClient 插件。

最后,作为一个技术细节,queries 被 hydrate 的时机略有变化。新的 queries 仍然在渲染阶段被 hydrate,以便 SSR 像往常一样工作,但是任何已经存在于缓存中的 queries 现在都在 effect 中被 hydrate(只要它们的数据比缓存中的数据更新鲜)。如果您像通常那样在应用程序启动时仅 hydrate 一次,这不会影响您,但是如果您正在使用 Server Components 并在页面导航时传递新的数据以进行 hydration,您可能会注意到在页面立即重新渲染之前旧数据的闪烁。

最后一个更改在技术上是一个 breaking change,它的目的是为了在我们完全提交页面过渡之前,不要过早地更新现有页面上的内容。您无需采取任何操作。

tsx
- import { Hydrate } from '@tanstack/react-query' // [!code --]
+ import { HydrationBoundary } from '@tanstack/react-query' // [!code ++]


- <Hydrate state={dehydratedState}> // [!code --]
+ <HydrationBoundary state={dehydratedState}> // [!code ++]
  <App />
- </Hydrate> // [!code --]
+ </HydrationBoundary> // [!code ++]
- import { Hydrate } from '@tanstack/react-query' // [!code --]
+ import { HydrationBoundary } from '@tanstack/react-query' // [!code ++]


- <Hydrate state={dehydratedState}> // [!code --]
+ <HydrationBoundary state={dehydratedState}> // [!code ++]
  <App />
- </Hydrate> // [!code --]
+ </HydrationBoundary> // [!code ++]

查询默认值变更

queryClient.getQueryDefaults 现在将合并所有匹配的注册,而不是仅返回第一个匹配的注册。

因此,对 queryClient.setQueryDefaults 的调用现在应该以递增的特异性排序。也就是说,注册应该从**最通用的 key** 到**最不通用的 key** 进行。

例如

ts
+ queryClient.setQueryDefaults(['todo'], {   // [!code ++]
+   retry: false,  // [!code ++]
+   staleTime: 60_000,  // [!code ++]
+ })  // [!code ++]
queryClient.setQueryDefaults(['todo', 'detail'], {
+   retry: true,  // [!code --]
  retryDelay: 1_000,
  staleTime: 10_000,
})
- queryClient.setQueryDefaults(['todo'], { // [!code --]
-   retry: false, // [!code --]
-   staleTime: 60_000, // [!code --]
- }) // [!code --]
+ queryClient.setQueryDefaults(['todo'], {   // [!code ++]
+   retry: false,  // [!code ++]
+   staleTime: 60_000,  // [!code ++]
+ })  // [!code ++]
queryClient.setQueryDefaults(['todo', 'detail'], {
+   retry: true,  // [!code --]
  retryDelay: 1_000,
  staleTime: 10_000,
})
- queryClient.setQueryDefaults(['todo'], { // [!code --]
-   retry: false, // [!code --]
-   staleTime: 60_000, // [!code --]
- }) // [!code --]

请注意,在这个特定的示例中,retry: true 已添加到 ['todo', 'detail'] 注册中,以抵消它现在从更通用的注册继承 retry: false。维护确切行为所需的具体更改将因您的 defaults 而异。

新功能 🚀

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

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

有限的无限查询,带有新的 maxPages 选项

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

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

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

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

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

用于 useQueries 的新 combine 选项

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

实验性的 fine grained storage persister

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

创建 Query Options 的类型安全方式

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

用于 suspense 的新钩子

在 v5 中,用于数据获取的 suspense 最终变得“稳定”。我们添加了专用的 useSuspenseQueryuseSuspenseInfiniteQueryuseSuspenseQueries hooks。使用这些 hooks,在类型级别上,data 将永远不会可能是 undefined

js
const { data: post } = useSuspenseQuery({
  // ^? const post: Post
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})
const { data: post } = useSuspenseQuery({
  // ^? const post: Post
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})

查询 hooks 上的实验性 suspense: boolean 标志已被移除。

您可以在 suspense 文档 中阅读有关它们的更多信息。