译零 TransZero · TanStack Query 中文文档
主站

迁移到 React Query 4

更新于 2026-11-02 React 查看英文原文 ↗

破坏性变更

v4 是一个主版本,因此存在一些你需要了解的破坏性变更:

react-query 现已更名为 @tanstack/react-query

你需要卸载/安装依赖项并更改导入语句:

Shell
1npm uninstall react-query2npm install @tanstack/react-query3npm install @tanstack/react-query-devtools
TypeScript
1- import { useQuery } from 'react-query' // [!code --]2- import { ReactQueryDevtools } from 'react-query/devtools' // [!code --]3 4+ import { useQuery } from '@tanstack/react-query' // [!code ++]5+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // [!code ++]

代码迁移工具(codemod)

为简化导入语句的迁移,v4 提供了一个代码迁移工具。

该代码迁移工具旨在尽可能协助你完成此次破坏性变更的迁移。请务必仔细审查生成的代码!此外,某些边界情况无法被代码迁移工具识别,因此请留意日志输出。

你可以通过以下任一(或两个)命令轻松应用该工具:

若要对 .js 或 .jsx 文件运行该工具,请使用下方命令:

Code
1npx jscodeshift ./path/to/src/ \2  --extensions=js,jsx \3  --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js

若要对 .ts 或 .tsx 文件运行该工具,请使用下方命令:

Code
1npx jscodeshift ./path/to/src/ \2  --extensions=ts,tsx \3  --parser=tsx \4  --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js

请注意:对于 TypeScript,你必须使用 tsx 作为解析器;否则,代码迁移工具将无法正确应用!

注意: 应用代码迁移工具可能会破坏你的代码格式,因此请务必在应用完该工具后运行 prettier 和/或 eslint!

注意: 该代码迁移工具 仅 修改导入语句——你仍需手动安装独立的开发者工具(devtools)包。

查询键(及修改键)必须为数组

在 v3 中,查询键和修改键可以是字符串或数组。React Query 内部始终仅使用数组形式的键,并且我们有时会将这一实现细节暴露给使用者。例如,在 queryFn 中,你始终会收到一个数组形式的键,以便更方便地配合 默认查询函数 使用。

然而,我们并未将这一设计原则贯彻到所有 API 中。例如,在 查询过滤器 上使用 predicate 函数时,你收到的是原始的查询键。如果你混用数组与字符串形式的查询键,这会使相关函数难以使用。全局回调函数也存在同样的问题。

为统一所有 API,我们决定强制所有键均为数组:

TypeScript
1;-useQuery('todos', fetchTodos) + // [!code --]2  useQuery(['todos'], fetchTodos) // [!code ++]

代码迁移工具(codemod)

为简化此项迁移,我们提供了对应的代码迁移工具。

该代码迁移工具旨在尽可能协助你完成此次破坏性变更的迁移。请务必仔细审查生成的代码!此外,某些边界情况无法被代码迁移工具识别,因此请留意日志输出。

你可以通过以下任一(或两个)命令轻松应用该工具:

若要对 .js 或 .jsx 文件运行该工具,请使用下方命令:

Code
1npx jscodeshift ./path/to/src/ \2  --extensions=js,jsx \3  --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js

若要对 .ts 或 .tsx 文件运行该工具,请使用下方命令:

Code
1npx jscodeshift ./path/to/src/ \2  --extensions=ts,tsx \3  --parser=tsx \4  --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js

请注意:对于 TypeScript,你必须使用 tsx 作为解析器;否则,代码迁移工具将无法正确应用!

注意: 应用代码迁移工具可能会破坏你的代码格式,因此请务必在应用完该工具后运行 prettier 和/或 eslint!

已移除 idle 状态

随着为增强离线支持而引入新的 fetchStatus,idle 状态已变得无关紧要,因为 fetchStatus: 'idle' 更准确地表达了相同的状态。更多信息请参阅 为何需要两种不同状态。

此变更主要影响尚未拥有任何 data 的 disabled 查询,因为这些查询此前处于 idle 状态:

TypeScript
1- status: 'idle' // [!code --]2+ status: 'loading'  // [!code ++]3+ fetchStatus: 'idle' // [!code ++]

此外,请参阅 依赖查询指南

已禁用的查询

受此变更影响,已禁用的查询(包括临时禁用的查询)将起始于 loading 状态。为便于迁移,尤其是为明确判断何时显示加载指示器(loading spinner),你可以检查 isInitialLoading 而非 isLoading:

TypeScript
1;-isLoading + // [!code --]2  isInitialLoading // [!code ++]

另请参阅 禁用查询指南

useQueries 的新 API

useQueries Hook 现在接受一个对象作为输入,该对象包含一个 queries 属性。queries 属性的值是一个查询数组(该数组与 v3 中传入 useQueries 的数组完全一致):

TypeScript
1;-useQueries([2  { queryKey1, queryFn1, options1 },3  { queryKey2, queryFn2, options2 },4]) + // [!code --]5  useQueries({6    queries: [7      { queryKey1, queryFn1, options1 },8      { queryKey2, queryFn2, options2 },9    ],10  }) // [!code ++]

undefined 不再是成功查询的合法缓存值

为支持通过返回 undefined 来跳过更新,我们必须将 undefined 设为非法的缓存值。这与其他 react-query 概念保持一致;例如,从 initialData 函数 返回 undefined 同样 不会 设置数据。

此外,在 queryFn 中添加日志记录时很容易意外产生 Promise<void>:

TypeScript
1useQuery(['key'], () =>2  axios.get(url).then((result) => console.log(result.data)),3)

现在,类型系统层面已禁止此行为;在运行时,undefined 将被转换为一个 失败的 Promise,这意味着你将收到一个 error,并且该错误也会在开发模式下打印到控制台。

查询与修改默认需要网络连接才能执行

请阅读关于在线/离线支持的 新特性公告,以及专门介绍 网络模式 的页面。

尽管 React Query 是一个异步状态管理器,可用于处理任何能返回 Promise 的操作,但它最常被用于结合数据拉取库进行数据拉取。因此,默认情况下,当无网络连接时,查询与修改将被 paused。若你想恢复此前的行为,可为查询与修改全局设置 networkMode: offlineFirst:

TypeScript
1new QueryClient({2  defaultOptions: {3    queries: {4      networkMode: 'offlineFirst',5    },6    mutations: {7      networkMode: 'offlineFirst',8    },9  },10})

notifyOnChangeProps 属性不再接受 "tracked" 作为取值

notifyOnChangeProps 选项不再接受 "tracked" 值。取而代之的是,useQuery 默认启用属性追踪。所有使用 notifyOnChangeProps: "tracked" 的查询都应通过移除此选项来更新。

若你希望在某些查询中绕过此机制,以模拟 v3 中“查询任意变更即触发重新渲染”的默认行为,则 notifyOnChangeProps 现在接受一个 "all" 值,用于退出默认的智能追踪优化。

notifyOnChangePropsExclusion 已被移除

在 v4 中,notifyOnChangeProps 默认采用 v3 的 "tracked" 行为,而非 undefined。既然 "tracked" 已成为 v4 的默认行为,保留此配置选项便不再有意义。

cancelRefetch 行为保持一致

cancelRefetch 选项可传递给所有用于命令式拉取查询的函数,即:

  • queryClient.refetchQueries
  • queryClient.invalidateQueries
  • queryClient.resetQueries
  • refetch(由 useQuery 返回)
  • fetchNextPage 与 fetchPreviousPage(由 useInfiniteQuery 返回)

除 fetchNextPage 与 fetchPreviousPage 外,该标志此前默认为 false,这种不一致可能带来问题:在修改后调用 refetchQueries 或 invalidateQueries 时,若此前已有较慢的拉取正在进行,则此次重新拉取可能被跳过,从而无法获得最新结果。

我们认为,若某段代码主动对某个查询执行重新拉取,则该拉取操作应默认重新开始。

因此,该标志现对上述所有方法均默认为 true。这也意味着,若你连续两次调用 refetchQueries 且未等待其完成,它将取消第一次拉取并以第二次调用重新开始:

Code
1queryClient.refetchQueries({ queryKey: ['todos'] })2// this will abort the previous refetch and start a new fetch3queryClient.refetchQueries({ queryKey: ['todos'] })

你可以通过显式传入 cancelRefetch:false 来退出此行为:

Code
1queryClient.refetchQueries({ queryKey: ['todos'] })2// this will not abort the previous refetch - it will just be ignored3queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })

注意:自动触发的拉取(例如因查询挂载或窗口聚焦而触发的重新拉取)的行为未发生变化。

查询过滤器

查询过滤器 是一个带有特定匹配条件的对象。历史上,过滤器选项大多是一组布尔标志的组合。但组合这些标志可能导致逻辑上不可能的状态。具体而言:

Code
1active?: boolean2  - When set to true it will match active queries.3  - When set to false it will match inactive queries.4inactive?: boolean5  - When set to true it will match inactive queries.6  - When set to false it will match active queries.

这些标志共同使用时效果不佳,因为它们彼此互斥。同时将 false 设为 true 可能匹配全部查询(按描述推断),也可能一个都不匹配,这显然不合逻辑。

在 v4 中,这些过滤器已被合并为单个过滤器,以更清晰地表达意图:

TypeScript
1- active?: boolean // [!code --]2- inactive?: boolean // [!code --]3+ type?: 'active' | 'inactive' | 'all' // [!code ++]

该过滤器默认为 all,你也可选择仅匹配 active 或 inactive 查询。

refetchActive / refetchInactive

queryClient.invalidateQueries 曾有两个额外的、功能相似的标志:

Code
1refetchActive: Boolean2  - Defaults to true3  - When set to false, queries that match the refetch predicate and are actively being rendered4    via useQuery and friends will NOT be refetched in the background, and only marked as invalid.5refetchInactive: Boolean6  - Defaults to false7  - When set to true, queries that match the refetch predicate and are not being rendered8    via useQuery and friends will be both marked as invalid and also refetched in the background

出于相同原因,这两个标志也已被合并:

TypeScript
1- refetchActive?: boolean // [!code --]2- refetchInactive?: boolean // [!code --]3+ refetchType?: 'active' | 'inactive' | 'all' | 'none' // [!code ++]

该标志默认为 active,因为 refetchActive 默认为 true。这意味着我们还需一种方式告知 invalidateQueries 完全不执行重新拉取,因此此处也允许第四个选项(none)。

onSuccess 不再由 setQueryData 调用

这对许多用户造成了困惑,且若在 onSuccess 内部调用 setQueryData,还会引发无限循环。此外,当与 staleTime 结合使用时,它也常导致错误:若数据仅从缓存读取,则 onSuccess 不会 被调用。

与 onError 和 onSettled 类似,onSuccess 回调现在绑定于实际请求的发起。无请求 → 无回调。

若你想监听 data 字段的变化,最佳方式是使用 useEffect,并将 data 加入其依赖数组。由于 React Query 通过结构共享确保数据稳定性,该 effect 不会在每次后台重新拉取时执行,而仅在 data 内部内容发生变更时才执行:

Code
1const { data } = useQuery({ queryKey, queryFn })2React.useEffect(() => mySideEffectHere(data), [data])

persistQueryClient 及对应持久化插件不再为实验性,并已重命名

插件 createWebStoragePersistor 和 createAsyncStoragePersistor 已分别重命名为 createSyncStoragePersister 和 createAsyncStoragePersister。persistQueryClient 中的接口 Persistor 也已重命名为 Persister。有关此项变更的背景,请参阅 此 StackExchange 讨论。

由于这些插件已不再是实验性功能,其导入路径也已更新:

TypeScript
1- import { persistQueryClient } from 'react-query/persistQueryClient-experimental' // [!code --]2- import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental' // [!code --]3- import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental' // [!code --]4 5+ import { persistQueryClient } from '@tanstack/react-query-persist-client' // [!code ++]6+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' // [!code ++]7+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'  // [!code ++]

Promise 上的 cancel 方法不再受支持

旧的 cancel 方法(允许你在 Promise 上定义 cancel 函数,库曾借此支持查询取消)已被移除。我们建议改用 新 API(自 v3.30.0 起引入)来实现查询取消,该 API 内部使用 AbortController API,并在你的查询函数中为你提供一个 AbortSignal 实例,以支持查询取消。

TypeScript

类型定义现要求使用 TypeScript v4.1 或更高版本。

支持的浏览器

自 v4 起,React Query 针对现代浏览器进行了优化。我们已更新 browserslist,以生成更现代、性能更好且体积更小的打包产物。你可在此处了解具体要求:here。

setLogger 已被移除

过去可通过调用 setLogger 全局更改日志器。在 v4 中,该函数已被替换为创建 QueryClient 时的一个可选字段。

TypeScript
1- import { QueryClient, setLogger } from 'react-query'; // [!code --]2+ import { QueryClient } from '@tanstack/react-query'; // [!code ++]3 4- setLogger(customLogger) // [!code --]5- const queryClient = new QueryClient(); // [!code --]6+ const queryClient = new QueryClient({ logger: customLogger }) // [!code ++]

服务端不再默认执行手动垃圾回收

在 v3 中,React Query 默认将查询结果缓存 5 分钟,然后手动执行垃圾回收。该默认行为同样适用于服务端 React Query。

这导致了高内存占用,以及进程因等待手动垃圾回收完成而挂起。在 v4 中,服务端 cacheTime 默认设为 Infinity,从而实质上禁用了手动垃圾回收(Node.js 进程会在请求完成后自动清理所有内容)。

此变更仅影响服务端 React Query 的使用者(例如使用 Next.js 的场景)。如果你已手动设置了 cacheTime,则不受影响(不过你可能希望与新行为保持一致)。

生产环境中的日志记录

自 v4 起,react-query 在生产模式下将不再向控制台输出错误日志(例如拉取失败),因为此前这一行为令许多用户感到困惑。错误日志仍会在开发模式下显示。

ESM 支持

React Query 现在支持 package.json "exports",并完全兼容 Node.js 原生的 CommonJS 与 ESM 解析机制。我们预计这对大多数用户不会构成破坏性变更,但此举限制了你可在项目中直接导入的文件范围——仅限我们官方支持的入口点。

简化后的 NotifyEvents

手动订阅 QueryCache 一直会为你提供一个 QueryCacheNotifyEvent,但 MutationCache 并非如此。我们已统一二者的行为,并相应调整了事件名称。

QueryCacheNotifyEvent

TypeScript
1- type: 'queryAdded' // [!code --]2+ type: 'added' // [!code ++]3- type: 'queryRemoved' // [!code --]4+ type: 'removed' // [!code ++]5- type: 'queryUpdated' // [!code --]6+ type: 'updated' // [!code ++]

MutationCacheNotifyEvent

MutationCacheNotifyEvent 使用与 QueryCacheNotifyEvent 相同的类型。

注意:仅当你通过 queryCache.subscribe 或 mutationCache.subscribe 手动订阅缓存时,此变更才相关。

已移除独立的注水导出项

自 3.22.0 版本起,注水工具已并入 React Query 核心。在 v3 中,你仍可继续使用来自 react-query/hydration 的旧导出项;但在 v4 中,这些导出项已被移除。

TypeScript
1- import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration' // [!code --]2+ import { dehydrate, hydrate, useHydrate, Hydrate } from '@tanstack/react-query' // [!code ++]

已从 queryClient、query 和 mutation 中移除未文档化的内部方法

QueryClient 上的 cancelMutations 和 executeMutation 方法此前未被文档化,且库内部也未使用,因此我们将其移除。由于它原本只是对 mutationCache 上某方法的封装,你仍可直接使用 executeMutation 对应的功能。

TypeScript
1- executeMutation< // [!code --]2-   TData = unknown, // [!code --]3-   TError = unknown, // [!code --]4-   TVariables = void, // [!code --]5-   TContext = unknown // [!code --]6- >( // [!code --]7-   options: MutationOptions<TData, TError, TVariables, TContext> // [!code --]8- ): Promise<TData> { // [!code --]9-   return this.mutationCache.build(this, options).execute() // [!code --]10- } // [!code --]

此外,query.setDefaultOptions 因未被使用而被移除;mutation.cancel 则因实际上无法取消正在进行的请求而被移除。

src/react 目录已重命名为 src/reactjs

此前,React Query 包含一个名为 react 的目录,其内容从 react 模块导入。这在某些 Jest 配置下可能引发问题,导致运行测试时出现如下错误:

Code
1TypeError: Cannot read property 'createContext' of undefined

目录重命名后,此类问题已不复存在。

如果你此前在项目中直接导入了 'react-query/react'(而非仅导入 'react-query'),则需更新你的导入语句:

TypeScript
1- import { QueryClientProvider } from 'react-query/react'; // [!code --]2+ import { QueryClientProvider } from '@tanstack/react-query/reactjs'; // [!code ++]

新特性 🚀

v4 带来了一系列令人兴奋的新特性:

支持 React 18

React 18 于今年早些时候发布,v4 现已原生支持 React 18 及其所带来的并发特性。

完善的离线支持

在 v3 中,React Query 始终会触发查询与修改,但随后便假定:若你想重试,则必须联网。这引发了若干令人困惑的情形:

  • 你处于离线状态并挂载一个查询——它进入 loading 状态,请求失败,且会持续停留在 loading 状态,直至你重新联网,尽管此时并未真正发起拉取;
  • 类似地,若你处于离线状态且已关闭重试功能,查询仍将触发并失败,最终进入 error 状态;
  • 你处于离线状态,却想触发一个未必需要网络连接的查询(因为你 可以 将 React Query 用于数据拉取之外的其他用途),但该查询因其他原因失败——此时查询将被暂停,直至你重新联网;
  • 若你处于离线状态,窗口焦点触发的重新拉取将完全不生效。

v4 中,React Query 引入了一种全新的 networkMode 来应对上述所有问题。请参阅关于新 Network mode 的专属页面以获取更多信息。

默认启用查询属性追踪

React Query 默认启用“追踪”查询属性,这将显著提升渲染性能。该功能自 v3.6.0 起即已存在,现已成为 v4 的默认行为。

使用 setQueryData 时可跳过更新

当使用 setQueryData 的函数式更新形式 时,你现在可通过返回 undefined 来跳过本次更新。这在 undefined 作为 previousValue 传入时尤为有用——这意味着当前尚无缓存条目,而你既不想也不具备条件创建新的条目,例如在切换待办事项完成状态的示例中:

TypeScript
1queryClient.setQueryData(['todo', id], (previousTodo) =>2  previousTodo ? { ...previousTodo, done: true } : undefined,3)

修改缓存的垃圾回收

修改现在也可像查询一样自动进行垃圾回收。修改的默认 cacheTime 同样设为 5 分钟。

多 Provider 下的自定义上下文

现在可指定自定义上下文,以确保 Hook 与其匹配的 Provider 正确配对。当组件树中可能存在多个 React Query Provider 实例时,这一点至关重要——你必须确保所用 Hook 绑定的是正确的 Provider 实例。

示例:

  1. 创建一个数据包。
TypeScript
1// Our first data package: @my-scope/container-data2 3const context = React.createContext<QueryClient | undefined>(undefined)4const queryClient = new QueryClient()5 6export const useUser = () => {7  return useQuery(USER_KEY, USER_FETCHER, {8    context,9  })10}11 12export const ContainerDataProvider = ({13  children,14}: {15  children: React.ReactNode16}) => {17  return (18    <QueryClientProvider client={queryClient} context={context}>19      {children}20    </QueryClientProvider>21  )22}
  1. 创建第二个数据包。
TypeScript
1// Our second data package: @my-scope/my-component-data2 3const context = React.createContext<QueryClient | undefined>(undefined)4const queryClient = new QueryClient()5 6export const useItems = () => {7  return useQuery(ITEMS_KEY, ITEMS_FETCHER, {8    context,9  })10}11 12export const MyComponentDataProvider = ({13  children,14}: {15  children: React.ReactNode16}) => {17  return (18    <QueryClientProvider client={queryClient} context={context}>19      {children}20    </QueryClientProvider>21  )22}
  1. 在你的应用中使用这两个数据包。
TypeScript
1// Our application2 3import { ContainerDataProvider, useUser } from "@my-scope/container-data";4import { AppDataProvider } from "@my-scope/app-data";5import { MyComponentDataProvider, useItems } from "@my-scope/my-component-data";6 7<ContainerDataProvider> // <-- Provides container data (like "user") using its own React Query provider8  ...9  <AppDataProvider> // <-- Provides app data using its own React Query provider (unused in this example)10    ...11      <MyComponentDataProvider> // <-- Provides component data (like "items") using its own React Query provider12        <MyComponent />13      </MyComponentDataProvider>14    ...15  </AppDataProvider>16  ...17</ContainerDataProvider>18 19// Example of hooks provided by the "DataProvider" components above:20const MyComponent = () => {21  const user = useUser() // <-- Uses the context specified in ContainerDataProvider.22  const items = useItems() // <-- Uses the context specified in MyComponentDataProvider23  ...24}

本页为 TanStack Query 文档(React 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de