TanStack Query(React Query)Network Mode 全解析:online / always / offlineFirst 三种联网模式的实现原理与实战配置
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
TanStack Query(本仓库中对应@tanstack/react-query、query-core等包)内置了三档networkMode,用于精确控制断网时 Query 与 Mutation 的行为:是直接不执行、照常执行,还是先执行一次再暂停重试。本文以 Network Mode 指南 为骨架,逐一拆解'online'、'always'、'offlineFirst'三种模式的语义、默认值与适用场景,并结合query-core源码(retryer.ts、onlineManager.ts、query.ts)讲清fetching / paused / idle与fetchStatus的底层切换逻辑,帮助你为离线 PWA、本地存储查询和普通在线应用选择正确的联网策略。
三种网络模式速览
TanStack Query 通过NetworkMode这一联合类型来区分在线 / 离线行为,其定义位于 types.ts:
export type NetworkMode = 'online' | 'always' | 'offlineFirst'| 模式 | 无网络时是否首次执行queryFn | 失败后重试 | refetchOnReconnect默认值 | 典型场景 |
|---|---|---|---|---|
'online'(默认) | 不执行,进入paused | 暂停,待恢复连接后继续 | true | 绝大多数在线数据请求 |
'always' | 总是执行 | 不暂停,失败直接进入error | false | 读本地AsyncStorage/ 纯内存计算,无需真实网络 |
'offlineFirst' | 执行一次 | 首次失败后暂停重试 | true | Service Worker 离线缓存、Cache-ControlHTTP 缓存 |
设置层级为“单条 Query / Mutation 局部配置,或通过 Query / Mutation 的全局默认值统一配置”,默认值固定为'online'。该类型同时被 QueryOptions 与 MutationOptions 引用,因此 Query 与 Mutation 两套体系共享同一套联网语义。
如何配置 networkMode
单条 Query 上设置
最常见的做法是直接在useQuery的选项中声明:
import { useQuery } from '@tanstack/react-query' // 本地优先:即使断网也尝试读取本地缓存 useQuery({ queryKey: ['local-counter'], queryFn: async () => { const stored = await readFromAsyncStorage('counter') if (stored !== null) { return stored } throw new Error('cache miss, need network') }, networkMode: 'offlineFirst', }) // 强制忽略在线状态 useQuery({ queryKey: ['computed-value'], queryFn: () => Promise.resolve(5), // 根本不需要网络 networkMode: 'always', })单条 Mutation 上设置
Mutation 与 Query 使用同一选项名:
import { useMutation } from '@tanstack/react-query' const sendLog = useMutation({ mutationFn: (payload) => api.send(payload), networkMode: 'always', // 例如发送到本地队列,不依赖真实网络 })全局默认值
既可以通过QueryClient的defaultOptions全局兜底,也可以配合queryClient.setQueryDefaults/setMutationDefaults按queryKey命中局部默认值:
import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { networkMode: 'online' }, // 全部查询默认值 mutations: { networkMode: 'online' }, // 全部变更默认值 }, }) queryClient.setQueryDefaults(['user', 'preferences'], { networkMode: 'offlineFirst' })关于默认值还有一个容易被忽略的依赖规则:在queryClient.defaultQueryOptions内部,当显式传入persister而未显式声明networkMode时,会自动将其推导为'offlineFirst'(见 queryClient.ts),这为持久化(persist)场景提供了合理的默认联网行为,具体在“与 Persister 的联动”一节展开。
Network Mode: online(默认,最常用)
'online'模式下,没有网络连接时 Query 与 Mutation 不会触发。这是默认模式,也是绝大多数数据获取库的合理选择。
如果某个查询在断网情况下被请求获取数据,它会在state(pending/error/success)中保持不变——也就是说查询并不会凭空失败,而是停留在当前状态等待网络恢复。与此同时,查询额外暴露了一维fetchStatus用来描述“fetch 管道本身的运行情况”,可能取值如下:
fetching:queryFn真正在执行,请求正在飞行中(in-flight);paused:查询未在执行,因断网而暂停,直到重新获得连接;idle:查询既没有在抓取,也没有被暂停。
为便于使用,isFetching与isPaused两个布尔量由fetchStatus派生而来,见 queryObserver.ts:
const isFetching = newState.fetchStatus === 'fetching' const isPaused = newState.fetchStatus === 'paused'注意:不要只看pending来渲染加载态
指南中特别强调了一处反直觉陷阱:首次挂载且断网时,查询可能处于state: 'pending'但fetchStatus: 'paused'。此时若 UI 只判断isPending就去渲染 loading spinner,页面会一直转圈而毫无意义,因此判断是否展示加载指示器时应结合fetchStatus:
const { isPending, fetchStatus } = useQuery({ queryKey: ['data'], queryFn: fetchData }) const showSpinner = isPending && fetchStatus === 'fetching' // 而不是只看 isPending抓取过程中掉线:暂停的是“重试”,不是查询本身
如果一个查询在在线时启动成功,但请求尚未返回时设备突然断网,TanStack Query 会暂停重试(retry)机制,待网络恢复后再继续执行(这是对中断 fetch 的继续continue,而非一次全新的refetch)。这在retryer中被实现为“可继续时继续,否则挂起等待”,其判定条件同时要求窗口聚焦且处于在线状态:
// packages/query-core/src/retryer.ts#L110-L113 const canContinue = () => focusManager.isFocused() && (config.networkMode === 'always' || onlineManager.isOnline()) && config.canRun()这条暂停/继续链路的关键点在于:
- 与
refetchOnReconnect相互独立。refetchOnReconnect(此模式下默认true)触发的是“重连后的重新抓取”,而这里发生的是一次“继续”动作。在 query.ts 的onOnline中可以看到两者同时被处理:先按shouldFetchOnReconnect触发refetch,再调用this.#retryer?.continue()唤醒被暂停的请求。 - 如果查询在这期间被取消(如组件卸载触发的静默取消,详见 Query Cancellation),那么它不会再继续——
cancel会使底层 retryer 的 Promise 以CancelledError收尾。
Network Mode: always(始终执行,忽略在线状态)
'always'模式下,TanStack Query始终发起抓取并完全忽略在线/离线状态。指南给出两个典型例子:queryFn内部只读取AsyncStorage,或者直接返回Promise.resolve(5)——即“查询本身根本不需要真实网络”的场景。
该模式的边界行为可从源码逐条得到印证:
- 查询永远不会因为断网而进入
paused。canFetch对'always'直接返回true:
// packages/query-core/src/retryer.ts#L53-L57 export function canFetch(networkMode: NetworkMode | undefined): boolean { return (networkMode ?? 'online') === 'online' ? onlineManager.isOnline() : true }- 重试也不会暂停:请求若失败,将按重试策略(默认最多 3 次,见
retryer.run中的config.retry ?? (isServerEnvironment() ? 0 : 3))走完,最终使查询进入error状态,不会卡在半路。 refetchOnReconnect默认为false:重连网络已不再是“陈旧数据应当刷新”的有效信号。这一默认值并非写死在选项里,而是在 queryClient.ts 的 defaultQueryOptions 中按模式推导出来的:
if (defaultedOptions.refetchOnReconnect === undefined) { defaultedOptions.refetchOnReconnect = defaultedOptions.networkMode !== 'always' }如果确实希望在'always'模式下依然响应重连,可以手动将refetchOnReconnect重新打开。
Network Mode: offlineFirst(折中方案)
'offlineFirst'是前两种模式之间的折中:TanStack Query 会让queryFn先执行一次,但之后的重试会被暂停。这一点对两类缓存场景非常友好:
- 带 Service Worker 拦截请求的离线优先 PWA:首屏请求可能命中 Service Worker 缓存而成功;
- 依赖
Cache-Control头的 HTTP 缓存:首次请求可能由浏览器 HTTP 缓存直接命中。
在这种情形下,如果第一次抓取来自离线存储/缓存并成功,查询正常完成;如果发生缓存未命中,真实网络请求发出后失败,此时该模式的表现退化为'online'——暂停后续重试,等待网络恢复。
这一“先试一次、失败再暂停”的策略同样可从源码验证:retryer.start()中,只要canFetch为真('offlineFirst'与'always'一样返回true),首次就会真正run()一次,只有进入失败重试路径的sleep之后才轮到canContinue/pause判定:
// packages/query-core/src/retryer.ts#L227-L235 start: () => { // Start loop if (canStart()) { run() } else { pause().then(run) } return promise },底层实现:retryer、OnlineManager 与恢复链路
OnlineManager:在线状态的唯一事实来源
onlineManager是全局单例(onlineManager.ts),其内部默认#online = true,并通过订阅window上的online/offline事件来更新状态(onlineManager.ts)。暴露的核心接口为:
onlineManager.isOnline(): boolean——读取当前在线状态;onlineManager.setOnline(online: boolean)——手动设置(Devtools 的 Mock 按钮正是走这条路);onlineManager.subscribe(listener)——订阅在线状态变化。
关于默认在线状态的说明可参考 onlineManager 参考文档:项目刻意不再依赖navigator.onLine作为初始值(Chromium 系浏览器中它有大量误判为离线的历史问题),而是默认online: true,仅靠事件驱动更新,以降低“假离线”误判概率;代价是经 Service Worker 加载的离线应用可能短暂出现“假在线”,因为它们确实可以在没有公网连接的情况下工作。
pause / continue 与状态机
在'online'模式下,若断网启动查询,canFetch返回false,retryer.start()进入pause()。pause()返回一个 Promise,其continueFn只有在isResolved()或canContinue()成立时才会放行,从而真正执行run();fetchStatus也随之在paused与fetching之间切换(对应 query.ts 中的状态 dispatch)。
恢复连接的完整链路是:window的online事件 →onlineManager广播 → Query 的onOnline()(见 query.ts)→ 触发refetchOnReconnect判断 +retryer.continue()→ 被暂停的抓取/重试继续执行。
Mutation 同样遵循 networkMode
Mutation 在构建其内部 retryer 时会把当前networkMode传入(见 mutation.ts),并且用!retryer.canStart()推导自身的 paused 状态(mutation.ts),因此 Mutation 也会在断网时暂停,并在恢复后继续,相关行为的完整指南见 Mutations。
与 Persister 的联动:自动降级为 offlineFirst
使用createPersister/persistQueryClient这类持久化方案时,写缓存通常依赖本地存储而不依赖网络。为了让持久化查询在“首次尝试本地读取、命中即成功、未命中才等网络”的语义下运行,defaultQueryOptions加入了自动推导规则(queryClient.ts):
if (!defaultedOptions.networkMode && defaultedOptions.persister) { defaultedOptions.networkMode = 'offlineFirst' }即:只要指定了persister且没有显式写死networkMode,就会被自动设置为'offlineFirst'。这意味着离线优先的持久化读取几乎开箱即用;若希望强制走'online'或'always',则需显式声明以覆盖该推导值。
Devtools:观察 paused 与模拟离线
TanStack Query Devtools 会以paused状态展示“本想抓取、但因断网未能执行”的查询,便于在开发阶段直接观察联网策略是否生效。
Devtools 面板中还提供了一个Mock offline behavior(模拟离线行为)的切换按钮。请特别注意:这个按钮并不会真的切断你的网络连接(真正断网可在浏览器 Devtools 的 Network 面板完成),它的实现只是把OnlineManager置为离线状态——源码中对应一次setOnline取反(见 query-devtools 的 Devtools.tsx):
onlineManager().setOnline(!onlineManager().isOnline())正因如此,通过该按钮可以零成本地验证你的 Query / Mutation 在三种networkMode下分别会表现出fetching、paused还是直接失败——与fetchStatus的观察相互印证。
Signature(配置签名速查)
networkMode的完整类型签名如下:
networkMode: 'online' | 'always' | 'offlineFirst'- 可选
- 默认值为
'online'
选型建议小结
- 常规在线请求:保持默认
'online',并记得结合fetchStatus === 'paused'正确渲染加载/离线提示; - 查询纯依赖本地数据(
AsyncStorage、内存计算):'always'; - 有 Service Worker 或
Cache-Control缓存兜底、愿意“先试一次、失败再等网络”的离线优先 PWA:'offlineFirst'; - 接入了
persister却未显式声明联网模式:框架已自动使用'offlineFirst',无需重复配置。
配合 Queries(fetchStatus与isPaused定义)、Mutations 以及 query-core 源码 阅读本文,即可完整掌握 TanStack Query 在离线场景下的行为边界。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考