React Native 中集成 TanStack Query(React 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
React Query(TanStack Query)被设计为可以在 React Native 中"开箱即用"。本指南基于当前仓库的框架文档与源码,系统讲解在 React Native 应用中落地 TanStack Query 时需要补齐的关键能力:把 Web 端的窗口焦点刷新、断网重连自动请求迁移到 AppState 与网络监听,以及如何在屏幕(Screen)层级精确控制查询的刷新与订阅。读完本文你将掌握onlineManager、focusManager、subscribed选项的正确用法,并能直接复用仓库中官方示例应用的完整封装模式。
React Native 与 Web 环境的差异:为什么要做额外适配
TanStack Query 在 Web 浏览器中默认就具备两套"自动重新请求"能力:
- 窗口重新聚焦时刷新(window focus refetching):用户切走又切回页面,若数据已过期,会自动在后台请求最新数据;
- 断网重连时刷新(reconnect refetching):网络从离线恢复在线时自动重新请求。
然而这两套能力在实现上都依赖浏览器全局对象。以仓库中 onlineManager.ts 的默认事件监听为例:
this.#setup = (onOnline) => { // addEventListener does not exist in React Native, but window does if (typeof window !== 'undefined' && window.addEventListener) { const onlineListener = () => onOnline(true) const offlineListener = () => onOnline(false) window.addEventListener('online', onlineListener, false) window.addEventListener('offline', offlineListener, false) return () => { window.removeEventListener('online', onlineListener) window.removeEventListener('offline', offlineListener) } } return }focusManager.ts 的默认实现同样监听window上的visibilitychange事件;而当手动聚焦状态未设置时,其isFocused()判断会退化为读取globalThis.document?.visibilityState——源码注释中明确指出"document global can be unavailable in react native"(focusManager.ts)。
结论很清晰:React Native 没有window/document,也没有online/offline浏览器事件,需要开发者用 RN 生态对应的模块把"在线状态"和"焦点状态"喂给 TanStack Query 的两个 Manager。框架文档 react-native.md 给出的正是这套适配方案,Web 端对应的完整背景可对照参考 window-focus-refetching.md。
React Native 场景下的开发者工具(DevTools)选项
框架文档将 React Native 的调试工具归类为几种社区方案,供不同工具链的团队选用(注意它们均为第三方项目,接入与维护需以各自项目自身为准):
- Rozenite Plugin:面向使用 React Native DevTools 的用户的第三方插件,支持在官方调试器中直接查看 TanStack Query 状态;
- Native macOS App:面向任意基于 JS 的应用的调试桌面应用,适合希望在原生调试视图中观察查询状态的场景;
- Flipper Plugin:面向 Facebook Flipper 用户的第三方插件;
- Reactotron Plugin:面向 Reactotron 用户的第三方插件,例如在 Reactotron 时间线中观察查询缓存与事件流。
选用时可根据团队当前使用的 RN 调试基础设施(官方 DevTools、Flipper 还是 Reactotron)决定,避免引入多套调试器叠加。
在线状态管理:让"断网重连自动刷新"在 RN 中生效
TanStack Query 通过单例onlineManager(onlineManager.ts)统一维护"是否在线"状态。setEventListener用于替换默认事件源:它会先执行上一次监听器的清理函数,再注册新的监听(onlineManager.ts)。当网络状态变化时调用setOnline(boolean),内部只会在状态确实改变时才通知所有订阅者(onlineManager.ts),从而触发"重连后重新请求"的行为。
方案一:使用 @react-native-community/netinfo
文档给出的标准做法是在模块顶层(或应用初始化处)用 NetInfo 替换默认事件源:
import NetInfo from '@react-native-community/netinfo' import { onlineManager } from '@tanstack/react-query' onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { setOnline(!!state.isConnected) }) })NetInfo.addEventListener返回的取消订阅函数直接作为setEventListener的回调返回值,恰好与onlineManager期望的"返回清理函数"签名一致——当再次调用setEventListener时,旧的 NetInfo 监听会被妥善移除。
方案二:使用 expo-network
使用 Expo 时没有 NetInfo 依赖,可以改监听expo-network的事件。由于网络状态事件可能存在"先注册、后补发初始值"的时序问题,文档示例用initialised标志做了一次兜底查询:
import { onlineManager } from '@tanstack/react-query' import * as Network from 'expo-network' onlineManager.setEventListener((setOnline) => { let initialised = false const eventSubscription = Network.addNetworkStateListener((state) => { initialised = true setOnline(!!state.isConnected) }) Network.getNetworkStateAsync() .then((state) => { if (!initialised) { setOnline(!!state.isConnected) } }) .catch(() => { // getNetworkStateAsync can reject on some platforms/SDK versions }) return eventSubscription.remove })先用addNetworkStateListener订阅状态变化事件;一旦收到首个事件就将initialised置为true。与此同时主动调用getNetworkStateAsync()查询当前网络状态,仅当事件监听尚未触发过(即首次同步初始化)时才回填在线状态,避免某些平台或 SDK 版本上事件未及时派发导致的初始状态缺失。
仓库示例中的增强版本
仓库内的官方 React Native 示例工程 useOnlineManager.ts 在上述模式上做了两处值得借鉴的增强:
import * as React from 'react' import NetInfo from '@react-native-community/netinfo' import { onlineManager } from '@tanstack/react-query' import { Platform } from 'react-native' export function useOnlineManager() { React.useEffect(() => { // React Query already supports on reconnect auto refetch in web browser if (Platform.OS !== 'web') { return NetInfo.addEventListener((state) => { onlineManager.setOnline( state.isConnected != null && state.isConnected && Boolean(state.isInternetReachable), ) }) } }, []) }一是用Platform.OS !== 'web'做守卫(RN Web 场景保留浏览器默认行为,无需覆盖);二是在isConnected之外额外要求isInternetReachable,避免"连着 WiFi 但实际无外网"的假在线状态触发无意义的重新请求。该 hook 在 App.tsx 的组件根部被调用,即完成了全应用级别的在线状态桥接。
App 级焦点管理:回到前台时自动刷新
Web 端默认通过visibilitychange感知页面可见性;React Native 则通过内置的AppState模块获取前台/后台信息。文档建议监听AppState的"change"事件,在应用回到active状态时同步给focusManager:
import { useEffect } from 'react' import { AppState, Platform } from 'react-native' import type { AppStateStatus } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, [])理解这段代码需要看到 focusManager.ts 的底层语义:
setFocused(focused?: boolean)只在状态确实变化时才派发事件,并调用所有监听器(focusManager.ts);- 当应用进入
active时setFocused(true)会触发一次全局"重新聚焦",此时仍处于过期(stale)状态的查询便会按refetchOnWindowFocus的逻辑在后台重新请求——该选项默认值为true(可对照 window-focus-refetching.md 中refetchOnWindowFocus: false // default: true的注释)。如果你不希望所有查询都跟随应用回到前台而刷新,可以在 QueryClient 的defaultOptions.queries里全局关闭refetchOnWindowFocus,或在具体useQuery上单独关闭。
示例工程将这一逻辑进一步封装成了可复用的 useAppState.ts:
import { useEffect } from 'react' import { AppState } from 'react-native' import type { AppStateStatus } from 'react-native' export function useAppState(onChange: (status: AppStateStatus) => void) { useEffect(() => { const subscription = AppState.addEventListener('change', onChange) return () => { subscription.remove() } }, [onChange]) }随后在 App.tsx 中定义onAppStateChange并调用useAppState(onAppStateChange),组件卸载时监听会自动移除,避免重复订阅或内存泄漏。
Screen 级焦点刷新:进入页面时重新拉取过期数据
AppState只能感知"整个 App"的前后台,无法感知"导航栈里当前显示的是哪个 Screen"。当业务希望在用户重新回到某个页面时刷新该页的数据(例如从详情页返回列表页),需要借助 React Navigation 的useFocusEffect。
文档推荐版本:刷新所有激活的过期查询
以下自定义 hook 会在 Screen 每次重新获得焦点时,refetch所有处于激活状态且已过期的查询:
import React from 'react' import { useFocusEffect } from '@react-navigation/native' import { useQueryClient } from '@tanstack/react-query' export function useRefreshOnFocus() { const queryClient = useQueryClient() const firstTimeRef = React.useRef(true) useFocusEffect( React.useCallback(() => { if (firstTimeRef.current) { firstTimeRef.current = false return } // refetch all stale active queries queryClient.refetchQueries({ queryKey: ['posts'], stale: true, type: 'active', }) }, [queryClient]), ) }代码中的refetchQueries是QueryClient提供的命令式刷新 API(实现见 queryClient.ts),这里三个过滤条件的含义分别是:
queryKey: ['posts']:限定只处理该键(或满足该前缀匹配)的查询;stale: true:只处理数据已过期的查询,避免无谓请求;type: 'active':只处理当前存在活跃订阅者(有组件正在观察)的查询,不打扰后台预取或缓存中的查询。
为什么要跳过第一次聚焦?因为useFocusEffect不仅在 Screen 重新聚焦时会调用回调,在 Screen 初次挂载时也会调用一次。初次挂载时数据通常本来就需要通过queryFn首次加载,没有必要再叠加一次refetch,因此用firstTimeRef跳过首次,只在"离开后再次回来"时刷新。
示例工程版本:按页面精准刷新
仓库示例 useRefreshOnFocus.ts 采用参数化设计,把"触发刷新"的动作下放给调用方:
import * as React from 'react' import { useFocusEffect } from '@react-navigation/native' export function useRefreshOnFocus(refetch: () => void) { const enabledRef = React.useRef(false) useFocusEffect( React.useCallback(() => { if (enabledRef.current) { refetch() } else { enabledRef.current = true } }, [refetch]), ) }使用时,页面把useQuery返回的refetch直接传入(见 MoviesListScreen.tsx):
const { isPending, error, data, refetch } = useQuery<Movie[], Error>({ queryKey: ['movies'], queryFn: fetchMovies, }) // ... useRefreshOnFocus(refetch)两种版本各有适用场景:文档版本适合"多个查询共享同一前缀、回到页面希望整体对齐"的场景;示例工程版本则粒度更细,配合enabledRef同样实现了首次聚焦(挂载)跳过、后续聚焦才刷新的语义。
焦点外的 Screen 暂停订阅:subscribed选项
如果不想让某些查询在 Screen 失焦后继续保持"活动"(例如还订阅着缓存更新、甚至触发定时刷新),TanStack Query 提供了subscribed选项。该选项设置为false时,组件会从查询中退订(unsubscribe),不再触发重新渲染,也不会为该 Screen 拉取新数据;当它重新变为true(例如 Screen 重新获得焦点)后,查询会重新订阅并保持最新。
结合 React Navigation 的useIsFocused,可以实现"焦点感知的订阅控制":
import React from 'react' import { useIsFocused } from '@react-navigation/native' import { useQuery } from '@tanstack/react-query' import { Text } from 'react-native' function MyComponent() { const isFocused = useIsFocused() const { dataUpdatedAt } = useQuery({ queryKey: ['key'], queryFn: () => fetch(...), subscribed: isFocused, }) return <Text>DataUpdatedAt: {dataUpdatedAt}</Text> }subscribed 的底层机制
该选项是react-query在QueryObserverOptions之上为useQuery/useInfiniteQuery等 Hook 增加的订阅开关(类型定义见 types.ts,默认值为true)。核心实现位于 useBaseQuery.ts:
const subscribed = options.subscribed !== false // ... defaultedOptions._optimisticResults = isRestoring ? 'isRestoring' : subscribed ? 'optimistic' : undefined // ... const shouldSubscribe = !isRestoring && subscribed React.useSyncExternalStore( React.useCallback( (onStoreChange) => { const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop // ... }, // ... ), )从源码可以清楚地看到两点:
- 订阅控制:当
subscribed === false时组件走noop分支,不再调用observer.subscribe,即没有活跃的 observer 挂到该查询上,后续缓存更新不会触发该组件重渲染,查询也会因没有活跃订阅者而停止被该页面驱动去获取数据; - 乐观状态:未订阅时
_optimisticResults不会被置为'optimistic',因此退订状态下不会"乐观地"把查询显示为 fetching 中的状态。
仓库配套的单元测试验证了这些行为(见 useQuery.test.tsx):当subscribed为false时,查询缓存中该查询的observers.length为 0、queryFn完全不会被调用;切换回true后订阅恢复,查询可再次正常触发。
使用限制与注意点
subscribed是useQuery级别的选项,但在useQueries中它只作为顶层选项出现,不支持在数组内逐条查询设置(见 useQueries.ts);useSuspenseQueries不支持该选项,需使用常规的useQueries(相关类型注释见 useSuspenseQueries.ts);- 当
subscribed: false时若页面还依赖初次数据渲染(例如首屏需要立刻展示),需自行配合placeholderData或缓存中已有数据,因为退订状态下查询不会主动发起首次请求。
完整组装:一个生产可用的 RN 应用骨架
把上述能力组合起来,参考示例工程 App.tsx,一个同时具备"断网重连刷新 + App 回到前台刷新"能力的根组件大致如下:
import * as React from 'react' import { AppStateStatus, Platform } from 'react-native' import { NavigationContainer } from '@react-navigation/native' import { QueryClient, QueryClientProvider, focusManager, } from '@tanstack/react-query' import { useAppState } from './src/hooks/useAppState' import { useOnlineManager } from './src/hooks/useOnlineManager' function onAppStateChange(status: AppStateStatus) { // React Query already supports in web browser refetch on window focus by default if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 2 } }, }) export default function App() { useOnlineManager() // 断网重连自动刷新(仅非 Web 生效) useAppState(onAppStateChange) // App 回到前台自动刷新 return ( <QueryClientProvider client={queryClient}> <NavigationContainer> {/* 你的导航栈,内部 Screen 可再结合 useRefreshOnFocus / subscribed 做页面级控制 */} </NavigationContainer> </QueryClientProvider> ) }各层职责清晰分层:
- 全局层(
App根部):onlineManager+focusManager桥接 RN 的系统级事件,保证缓存数据在重连、回前台时自动对齐; - 页面层:
useRefreshOnFocus处理"从其他 Screen 返回"的场景;subscribed+useIsFocused处理"失焦页面不再保持订阅"的资源节省场景; - 数据层:
refetchQueries的stale/type: 'active'过滤条件保证每次刷新都只命中"真正需要"的查询。
这套组合正是官方 React Native 示例(examples/react/react-native)采用的工程范式,可直接作为新项目的脚手架参考。
小结
TanStack Query 之所以能在 React Native 中"开箱即用",是因为其核心设计把浏览器环境依赖全部收敛在onlineManager与focusManager两个可替换的事件源单例中。开发者只需按本文模式用 RN 生态模块替换默认事件源,即可在原生环境中完整获得 Web 端同款的自动刷新能力:
| 能力 | Web 端默认事件源 | React Native 替代方案 | 仓库证据 |
|---|---|---|---|
| 重连自动刷新 | window的online/offline | onlineManager.setEventListener+ NetInfo / expo-network | onlineManager.ts |
| 回前台自动刷新 | document的visibilitychange | focusManager.setFocused+AppState | focusManager.ts |
| 页面聚焦刷新 | — | useFocusEffect+refetchQueries(stale/type: 'active') | queryClient.ts |
| 失焦页暂停更新 | — | subscribed选项 +useIsFocused | useBaseQuery.ts |
调试时可按团队既有工具链在上述社区 DevTools 方案中选择其一;需要进一步验证行为时,可直接运行仓库中的 React Native 示例工程,并参考其src/hooks下各 hook 的写法抽取到自己的项目中使用。
【免费下载链接】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),仅供参考