框架
版本

迁移到 TanStack Query v5

重大更改

v5 是一个主版本,所以有一些破坏性更改需要注意

支持单一签名,一个对象

useQuery 及其相关 hook 在 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 提供了一个代码转换工具。

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

如果您想在 .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”属性(取决于正在转换的 hook/方法调用)。如果是这种情况,您的代码已经符合新签名,因此代码转换工具不会对其进行修改。🎉
  • 如果上述条件不满足,那么代码转换工具将检查第一个参数是数组表达式还是引用数组表达式的标识符。如果是这种情况,代码转换工具将将其放入对象表达式中,然后将其作为第一个参数。
  • 如果可以推断出对象参数,代码转换工具将尝试将现有属性复制到新创建的对象中。
  • 如果代码转换工具无法推断出用法,它将在控制台留下消息。该消息包含文件名和使用行的行号。在这种情况下,您需要手动进行迁移。
  • 如果转换导致错误,您也会在控制台看到一条消息。这条消息会通知您发生了意外情况,请手动进行迁移。

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.0 版本中已被弃用,在此版本中已被移除。日志记录仅在开发模式下有效,在这种模式下,无需传递自定义日志记录器。

支持的浏览器

我们已更新浏览器列表以生成更现代、更高效、更小的包。您可以在 此处 阅读有关要求的信息。

私有类字段和方法

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 hook 前缀“use”以及“ErrorBoundary”组件名称产生混淆,它已重命名为 throwOnError,以更准确地反映其功能。

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

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

如果您想 throw 非 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 标志,因为它们的功能与 placeholderDataisPlaceholderData 标志非常相似。

为了实现与 keepPreviousData 相同的功能,我们在 placeholderData 中添加了前一个查询 data 作为参数,该参数接受一个身份函数。因此,您只需为 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 属性,转而使用自定义 queryClient 实例

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

但是,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。要获得以前不进行查询/突变脱水行为,请传入 () => 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 更改为 pending,并且 isLoading 标志已更改为 isPending

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

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

hashQueryKey 已重命名为 hashKey

因为它也哈希 mutation keys,并且可以在 useIsMutatinguseMutationStatepredicate 函数中使用,这些函数会传递 mutations。

最低要求的 React 版本现在是 18.0

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

contextSharing 属性已从 QueryClientProvider 中移除

以前,您可以使用 contextSharing 属性来跨窗口共享第一个(至少一个)query client 实例。这确保了如果 TanStack Query 在不同的 bundle 或微前端中使用,它们都将使用相同的 context 实例,而不管模块作用域。

随着 v5 中自定义 context 属性的移除,请参阅关于“移除了自定义 context 属性,转而使用自定义 queryClient 实例”的部分。如果您希望在应用程序的多个包之间共享同一个 query client,您可以直接传递一个共享的自定义 queryClient 实例。

不再将 unstable_batchedUpdates 用作 React 和 React Native 中的批处理函数

由于 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)

Hydration API 更改

为了更好地支持并发功能和转换,我们对 hydration API 进行了一些更改。Hydrate 组件已重命名为 HydrationBoundary,并且已移除 useHydrate hook。

HydrationBoundary 不再 hydration mutations,仅 hydration queries。要 hydration mutations,请使用低级别的 hydrate API 或 persistQueryClient 插件。

最后,作为技术细节,查询 hydration 的时机略有改变。新查询仍在渲染阶段 hydration,以便 SSR 正常工作,但任何已存在于缓存中的查询现在都在 effect 中 hydration(只要其数据比缓存中的数据更新)。如果您只在应用程序开始时进行一次 hydration,这通常不会影响您,但如果您使用 Server Components 并在页面导航时传递更新的数据进行 hydration,您可能会在页面立即重新渲染之前注意到旧数据的闪烁。

最后这个更改在技术上是一个破坏性更改,它被做出是为了避免在页面转换完全提交之前过早地更新*现有*页面上的内容。您无需采取任何行动。

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

Query 默认值更改

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

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

例如

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 的情况。为保持行为不变而需要进行的具体更改将取决于您的默认设置。

新功能 🚀

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

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

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

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

useQueries 的新 combine 选项

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

实验性的 fine grained storage persister

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

创建 Query Options 的类型安全方式

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

用于 suspense 的新 hook

使用 v5,数据获取的 suspense 最终变得“稳定”。我们添加了专用的 useSuspenseQueryuseSuspenseInfiniteQueryuseSuspenseQueries hook。使用这些 hook,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),
})

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

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