框架
版本

迁移到 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)上的回调已被删除

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

refetchInterval 回调函数仅传递 query

这简化了回调的调用方式(refetchOnWindowFocusrefetchOnMountrefetchOnReconnect 回调也都只传递查询),并且修复了一些当回调获取由 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 标志,因为它们的功能与 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, // 与 `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 开始,并且只侦听 onlineoffline 事件来更新状态。

这应该会降低假阴性的可能性,但是,对于通过 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 的选项已简化。查询和变更始终根据默认函数实现进行脱水。要更改此行为,请不要使用已删除的布尔选项 dehydrateMutationsdehydrateQueries,而是实现等效的函数 shouldDehydrateQueryshouldDehydrateMutation。要获得完全不水合查询/变更的旧行为,请传入 () => 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 值直接传递给 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 更改为 pendingisLoading 标志已更改为 isPending

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

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

hashQueryKey 已重命名为 hashKey

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

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

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

QueryClientProvider 中的 contextSharing 属性已被删除

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

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

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

由于函数 unstable_batchedUpdates 在 React 18 中是空操作,因此它将不再自动设置为 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 更改

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

HydrationBoundary 不再水合变更,仅水合查询。要水合变更,请使用低级 hydrate API 或 persistQueryClient 插件。

最后,作为一个技术细节,查询水合的时间略有更改。新查询仍在渲染阶段进行水合,以便 SSR 照常工作,但缓存中已存在的任何查询现在都在效果中进行水合(只要它们的数据比缓存中的数据更新)。如果您像通常那样在应用程序启动时只进行一次水合,则不会受到影响,但是如果您正在使用服务器组件并在页面导航时向下传递新的水合数据,则可能会在页面立即重新渲染之前注意到旧数据的闪烁。

最后这个更改在技术上是一个重大更改,是为了避免在页面转换完全提交之前过早地更新_现有_页面上的内容而进行的。您无需执行任何操作。

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 的调用现在应按特异性_递增_的顺序排列。 也就是说,应从最通用的键最不通用的键进行注册。

例如:

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

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

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

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

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

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

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

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

useQueries 的新 combine 选项

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

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

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

创建查询选项的类型安全方式

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

用于 suspense 的新钩子

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

查询钩子上的实验性 suspense: boolean 标志已被删除。

您可以在 suspense 文档中阅读更多相关信息。