@qiaopeng/tanstack-query-plus
v0.5.10
Published
Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, data guard, utils
Maintainers
Readme
@qiaopeng/tanstack-query-plus 完整使用教程
本教程将带你从零开始,循序渐进地学习如何使用
@qiaopeng/tanstack-query-plus。每个章节都会自然地引出下一个概念,帮助你建立完整的知识体系。
目录
- 前言:为什么需要这个库?
- 安装与环境准备
- 第一步:配置 Provider
- 第二步:发起你的第一个查询
- 第三步:使用增强查询追踪性能
- 第四步:管理 Query Key
- 第五步:数据变更与乐观更新
- 第六步:无限滚动与分页
- 第七步:全局状态与 Mutation 监控
- 第八步:批量查询与仪表盘
- 第九步:智能预取
- 第十步:Suspense 模式与 SSR
- 第十一步:离线支持与持久化
- 第十二步:数据防护与安全
- 第十三步:焦点管理
- 第十四步:工具函数与选择器
- 最佳实践与常见问题
- API 索引
1. 前言:为什么需要这个库?
在使用 TanStack Query(原 React Query)时,你可能会遇到以下问题:
- 配置繁琐:每次新项目都要重新配置 staleTime、gcTime、重试策略等
- 缺乏最佳实践:不确定什么样的配置才是最优的
- 重复代码:乐观更新、错误处理、性能追踪等逻辑需要反复编写
- 离线支持复杂:实现离线队列和数据持久化需要大量代码
@qiaopeng/tanstack-query-plus 就是为了解决这些问题而生的。它在 TanStack Query v5 的基础上,提供了:
- 🚀 开箱即用的最佳实践配置
- 🔄 增强的 Hooks(性能追踪、慢查询检测、错误日志)
- 💾 一键启用的持久化
- 📡 完整的离线支持
- ⚡ 多种智能预取策略
- 🎯 内置乐观更新
接下来,让我们一步步学习如何使用这些功能。
1.1 设计初衷与原则
- 保持与 TanStack Query v5 完全兼容,不改变其核心行为,只做“安全增强”。
- 提供开箱即用的最佳实践配置,减少重复劳动与认知负担。
- 以“安全”为首要前提:数据防护、持久化安全、离线队列的稳健性、错误处理的可控性。
- API 设计坚持渐进增强:原生用法不变,增强能力按需启用,便于迁移和学习。
- TypeScript 友好:导出类型与范型参数与 TanStack 保持一致,避免类型陷阱。
1.2 适用场景
- 中大型前端应用,需要统一的查询管理与最佳实践配置。
- 有离线需求(电商、文档编辑、移动端 Web)或需要缓存持久化与恢复的场景。
- 需要更强的乐观更新、并发冲突处理、数据一致性保障。
- 希望最小化自定义基础设施代码,将精力聚焦在业务逻辑。
1.3 非目标与边界
- 不替代后端的并发控制与数据一致性保障;前端 Data Guard 仅作为“最后防线”。
- 不内置与具体后端协议的强绑定(如 GraphQL/REST 的特定实现);保持通用。
- 不存储任何敏感凭据;持久化仅针对查询缓存,且可配置与可关闭。
1.4 安全与合规
- 持久化默认仅保存可序列化且成功的查询数据,避免异常对象导致恢复失败(参见
createPersistOptions)。 - 建议不要在
queryKey中包含敏感信息(如 token、身份证号、手机号原文)。 - DevTools 仅在开发环境启用,避免在生产泄露内部状态(参见
isDevToolsEnabled)。 - 离线队列持久化时去除了函数体,仅存操作元数据;实际执行函数需通过注册表安全绑定。
1.5 术语速览
queryKey:查询的唯一标识;必须是稳定、可序列化的值(通常为数组)queryFn:实际获取数据的异步函数;返回 PromisestaleTime:数据保持“新鲜”的时间窗口;新鲜期内不会重复请求gcTime:缓存保留时间;超过后缓存会被清理invalidate:标记查询为过期;下一次渲染或焦点恢复时会重新请求refetch:主动重新请求数据persist:将查询缓存持久化到存储(localStorage/IndexedDB)并在刷新后恢复offline:网络不可用时的状态;本库提供队列与自动恢复机制optimistic update:先更新 UI,再与服务端同步;失败时需回滚Data Guard:防止旧数据覆盖新数据的前端机制(版本/时间戳/哈希比对)
2. 安装与环境准备
2.1 安装核心依赖
首先,安装必需的包:
npm install @qiaopeng/tanstack-query-plus @tanstack/react-query @tanstack/react-query-persist-client这三个包的作用分别是:
@qiaopeng/tanstack-query-plus:本库,提供增强功能@tanstack/react-query:TanStack Query 核心库@tanstack/react-query-persist-client:持久化支持
2.2 安装可选依赖
根据你的需求,可以选择安装以下可选依赖:
# 开发调试工具(强烈推荐在开发环境使用)
npm install @tanstack/react-query-devtools
# 视口预取功能(如果需要 useInViewPrefetch)
npm install react-intersection-observer
# 路由预取示例所需(如果使用 useRoutePrefetch 示例中的 Link/useNavigate)
npm install react-router-dom2.3 环境要求
确保你的项目满足以下要求:
- Node.js >= 16
- React >= 18
- TypeScript(推荐,但非必需)
现在环境准备好了,让我们开始配置应用。
2.4 学习路径与检查清单
严格建议按照以下顺序学习与落地,并在每一步完成后进行自检:
- 安装依赖:确保安装本库及 peer 依赖(
@tanstack/react-query、react、react-dom),按需安装devtools、react-intersection-observer、react-router-dom - 创建
QueryClient:使用GLOBAL_QUERY_CONFIG,避免随意调整retry、staleTime造成请求风暴 - 包裹应用:使用
PersistQueryClientProvider开启持久化与离线支持(生产环境建议保留持久化) - 添加 DevTools(开发环境):
isDevToolsEnabled()控制显示,严禁在生产强制开启 - 发起首个查询:优先使用
useEnhancedQuery,在慢查询或错误场景验证日志输出 - 增强 Mutation:在列表 CRUD 场景启用乐观更新,并验证回滚路径与错误处理
- 离线与持久化:断网测试页面行为;验证缓存恢复与离线队列的稳健性
- 数据防护(可选但推荐):开启 Data Guard 的版本/时间戳/哈希策略,防止旧数据覆盖
- 焦点管理:按照业务需要控制窗口聚焦时的刷新频率,避免抖动
- 预取:根据网络情况与页面流量,按需启用悬停/视口/路由/空闲预取,避免过度预取
完成以上 10 点后,再进入“最佳实践与常见问题”章节进行整体检查与性能、安全优化。
2.5 五分钟上手示例
目标:用 5 分钟完成“配置 Provider + 首个查询 + DevTools 调试”。
- 安装依赖:
npm install @qiaopeng/tanstack-query-plus @tanstack/react-query @tanstack/react-query-persist-client
npm install @tanstack/react-query-devtools --save-dev- 创建 Provider:
// main.tsx
import { QueryClient, PersistQueryClientProvider } from '@qiaopeng/tanstack-query-plus'
import { GLOBAL_QUERY_CONFIG } from '@qiaopeng/tanstack-query-plus/core'
import { ReactQueryDevtools, isDevToolsEnabled } from '@qiaopeng/tanstack-query-plus/core/devtools'
const queryClient = new QueryClient({ defaultOptions: GLOBAL_QUERY_CONFIG })
function Providers({ children }) {
return (
<PersistQueryClientProvider client={queryClient}>
{children}
{isDevToolsEnabled() && <ReactQueryDevtools initialIsOpen={false} />}
</PersistQueryClientProvider>
)
}- 发起首个查询:
// App.tsx
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
export default function App() {
const { data, isLoading, isError } = useEnhancedQuery({
queryKey: ['hello'],
queryFn: async () => ({ message: 'Hello Query Plus' }),
})
if (isLoading) return <div>加载中...</div>
if (isError) return <div>加载失败</div>
return <div>{data.message}</div>
}- 跑起来:在浏览器中打开 DevTools 面板,查看
['hello']查询状态。
2.6 TypeScript 配置建议
以下 tsconfig.json 选项可以帮助初学者避免常见类型问题:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Node",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"useUnknownInCatchVariables": true
}
}说明:
strict: true有助于暴露隐含的any与未处理的undefinedskipLibCheck: true可避免第三方库类型检查的噪音(对本库安全)useUnknownInCatchVariables提醒你显式处理错误类型
3. 第一步:配置 Provider
任何使用 TanStack Query 的应用都需要一个 Provider 来提供 QueryClient 实例。本库提供了一个增强版的 Provider,让配置变得更简单。
3.1 最简配置
最简单的配置只需要几行代码:
// App.tsx
import { QueryClient, PersistQueryClientProvider } from '@qiaopeng/tanstack-query-plus'
import { GLOBAL_QUERY_CONFIG } from '@qiaopeng/tanstack-query-plus/core'
// 创建 QueryClient,使用预配置的最佳实践
const queryClient = new QueryClient({
defaultOptions: GLOBAL_QUERY_CONFIG
})
function App() {
return (
<PersistQueryClientProvider client={queryClient}>
<YourApp />
</PersistQueryClientProvider>
)
}这段代码做了什么?
- 创建 QueryClient:使用
GLOBAL_QUERY_CONFIG预配置,包含了经过优化的默认值 - 包裹应用:
PersistQueryClientProvider让所有子组件都能访问 QueryClient
3.2 启用持久化和离线支持
PersistQueryClientProvider 默认就启用了持久化和离线支持(enablePersistence 和 enableOfflineSupport 默认都是 true)。如果你想显式配置或禁用某些功能:
<PersistQueryClientProvider
client={queryClient}
enablePersistence={true} // 启用 localStorage 持久化(默认 true)
enableOfflineSupport={true} // 启用离线状态监听(默认 true)
cacheKey="my-app-cache" // 自定义缓存 key(默认 'tanstack-query-cache')
onPersistRestore={() => console.log('缓存已恢复')} // 缓存恢复回调
onPersistError={(err) => console.error('持久化错误', err)}
>
<YourApp />
</PersistQueryClientProvider>enablePersistence 的作用:
- 自动将查询缓存保存到 localStorage
- 页面刷新后自动恢复缓存数据
- 用户可以立即看到上次的数据,无需等待网络请求
- 设为
false可禁用持久化
enableOfflineSupport 的作用:
- 监听网络状态变化
- 离线时暂停请求,在线时自动恢复
- 配合离线队列管理器使用
- 设为
false可禁用离线支持
3.3 理解预配置
GLOBAL_QUERY_CONFIG 包含了以下默认值:
{
queries: {
staleTime: 30000,
gcTime: 600000,
retry: defaultQueryRetryStrategy, // 智能重试:4XX不重试,5XX最多1次
retryDelay: exponentialBackoff,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
},
mutations: {
retry: 0, // Mutation 默认不重试
retryDelay: exponentialBackoff,
gcTime: 600000,
}
}重试策略说明:
- Query 重试(
defaultQueryRetryStrategy):- 4XX 客户端错误:不重试(客户端问题,重试无意义)
- 5XX 服务端错误:最多重试 1 次(避免过度重试)
- 网络错误:最多重试 2 次
- Mutation 重试(
defaultMutationRetryStrategy):- 4XX 客户端错误:不重试
- 5XX 服务端错误:不重试(避免重复操作)
- 网络错误:最多重试 1 次
这些值是经过实践验证的最佳实践,适合大多数应用场景。
3.4 根据环境选择配置
本库还提供了针对不同环境的预配置:
import { getConfigByEnvironment } from '@qiaopeng/tanstack-query-plus/core'
// 根据环境自动选择配置
const env =
process.env.NODE_ENV === 'production'
? 'production'
: process.env.NODE_ENV === 'test'
? 'test'
: 'development'
const config = getConfigByEnvironment(env)
const queryClient = new QueryClient({ defaultOptions: config })不同环境的配置差异:
| 配置项 | development | production | test | |--------|-------------|------------|------| | staleTime | 0 | 10 分钟 | 0 | | retry (Query) | 智能重试* | 智能重试* | 0 | | retry (Mutation) | 0 | 0 | 0 | | refetchOnWindowFocus | true | true | false |
*智能重试:4XX 不重试,5XX 最多 1 次,网络错误最多 2 次
补充:还支持 getConfigByEnvironment('longCache') 与 getConfigByEnvironment('realtime') 两种预设,分别适用于“长缓存”与“高实时”场景。
3.5 自定义重试策略
如果默认的重试策略不满足你的需求,可以使用 createSafeRetryStrategy 和 createErrorSafeConfig 来自定义:
import {
createSafeRetryStrategy,
createErrorSafeConfig
} from '@qiaopeng/tanstack-query-plus/core'
// 方式一:创建自定义重试策略
const customRetry = createSafeRetryStrategy(
0, // 4XX 错误重试次数
1, // 5XX 错误重试次数
2 // 其他错误重试次数
)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: customRetry,
}
}
})
// 方式二:使用错误安全配置(推荐)
const errorSafeConfig = createErrorSafeConfig({
maxRetries4xx: 0, // 4XX 不重试
maxRetries5xx: 0, // 5XX 不重试(严格模式)
maxRetriesOther: 1, // 网络错误最多 1 次
disableFocus: false, // 是否禁用窗口聚焦时 refetch
disableReconnect: false // 是否禁用重连时 refetch
})
const queryClient = new QueryClient({
defaultOptions: errorSafeConfig
})使用场景:
- 严格模式:完全禁用 4XX/5XX 重试,避免不必要的请求
- 宽松模式:增加重试次数,适合网络不稳定的环境
- 自定义场景:根据业务需求精确控制重试行为
3.6 添加 DevTools(开发环境)
在开发环境中,强烈建议添加 DevTools 来调试查询状态:
import { ReactQueryDevtools, isDevToolsEnabled } from '@qiaopeng/tanstack-query-plus/core/devtools'
function App() {
return (
<PersistQueryClientProvider client={queryClient}>
<YourApp />
{isDevToolsEnabled() && <ReactQueryDevtools initialIsOpen={false} />}
</PersistQueryClientProvider>
)
}DevTools 可以让你:
- 查看所有查询的状态
- 手动触发 refetch
- 查看缓存数据
- 调试查询问题
现在 Provider 配置好了,让我们开始发起第一个查询!
4. 第二步:发起你的第一个查询
配置好 Provider 后,我们就可以在组件中使用查询了。
4.1 基础查询
最基本的查询可以使用 TanStack Query 原生的 useQuery,或者本库提供的增强版 useEnhancedQuery:
// 方式一:使用 TanStack Query 原生 useQuery
import { useQuery } from '@tanstack/react-query'
// 方式二:使用本库的增强版(推荐,支持性能追踪等功能)
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
// 两者用法相同,useEnhancedQuery 额外支持性能追踪
const { data, isLoading, isError, error } = useEnhancedQuery({
queryKey: ['user', userId], // 查询的唯一标识
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()), // 获取数据的函数
})
if (isLoading) return <div>加载中...</div>
if (isError) return <div>错误: {error.message}</div>
return <div>用户名: {data.name}</div>
}关键概念解释:
queryKey:查询的唯一标识符,是一个数组。TanStack Query 用它来:
- 缓存数据
- 判断是否需要重新请求
- 在多个组件间共享数据
queryFn:实际获取数据的异步函数。可以是 fetch、axios 或任何返回 Promise 的函数。
返回值:
data:查询成功后的数据isLoading:首次加载中isError:是否出错error:错误对象
4.2 条件查询
有时候我们需要在满足某些条件时才发起查询:
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
const { data } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // 只有 userId 存在时才查询
})
// ...
}4.3 使用 skipToken 禁用查询
另一种禁用查询的方式是使用 skipToken:
import { useEnhancedQuery, skipToken } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
const { data } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: userId ? () => fetchUser(userId) : skipToken,
})
// ...
}注意:skipToken 也可以从 @qiaopeng/tanstack-query-plus 主包导入,或者从 @tanstack/react-query 导入。
skipToken 的好处是 TypeScript 类型推断更准确。
4.4 自定义缓存时间
你可以为特定查询设置不同的缓存策略:
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
const { data } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 分钟内数据视为新鲜
gcTime: 30 * 60 * 1000, // 缓存保留 30 分钟
})staleTime vs gcTime 的区别:
- staleTime:数据被认为是"新鲜"的时间。在这段时间内,即使组件重新挂载,也不会重新请求。
- gcTime:数据在缓存中保留的时间。超过这个时间,数据会被垃圾回收。
现在你已经会发起基本查询了。但在实际项目中,我们往往需要追踪查询性能、检测慢查询。这就是增强查询的用武之地。
5. 第三步:使用增强查询追踪性能
useEnhancedQuery 是本库的核心 Hook 之一,它在原生 useQuery 的基础上增加了性能追踪、慢查询检测和错误日志功能。
5.1 基本使用
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
const {
data,
isLoading,
isError,
error,
// 增强的返回值
refetchCount, // 重新获取次数
lastQueryDuration // 最后一次查询耗时(毫秒)
} = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
if (isLoading) return <div>加载中...</div>
if (isError) return <div>错误: {error.message}</div>
return (
<div>
<h1>{data.name}</h1>
<p className="text-sm text-gray-500">
查询耗时: {lastQueryDuration}ms | 刷新次数: {refetchCount}
</p>
</div>
)
}5.2 启用性能追踪
要追踪查询性能,需要显式启用 trackPerformance:
const { data, lastQueryDuration } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
trackPerformance: true, // 启用性能追踪
})启用后,lastQueryDuration 会记录每次查询的耗时。
5.3 检测慢查询
在生产环境中,检测慢查询对于性能优化至关重要:
const { data } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
trackPerformance: true,
slowQueryThreshold: 2000, // 超过 2 秒视为慢查询
onSlowQuery: (duration, queryKey) => {
// 上报到监控系统
analytics.track('slow_query', {
queryKey: JSON.stringify(queryKey),
duration,
})
console.warn(`慢查询警告: ${JSON.stringify(queryKey)} 耗时 ${duration}ms`)
},
})实际应用场景:
- 性能监控:将慢查询上报到 APM 系统(如 Sentry、DataDog)
- 开发调试:在开发环境中快速发现性能问题
- 用户体验优化:识别需要优化的 API 接口
5.4 错误日志
useEnhancedQuery 默认在开发环境自动记录错误:
const { data } = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
logErrors: true, // 默认在开发环境为 true
})当查询出错时,控制台会输出:
[useEnhancedQuery Error] ["user","123"]: Error: Network request failed如果你想在生产环境禁用错误日志:
logErrors: process.env.NODE_ENV === 'development'5.5 完整示例:带监控的用户详情页
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserDetailPage({ userId }) {
const {
data: user,
isLoading,
isError,
error,
refetchCount,
lastQueryDuration,
refetch
} = useEnhancedQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('获取用户失败')
return response.json()
},
trackPerformance: true,
slowQueryThreshold: 3000,
onSlowQuery: (duration, queryKey) => {
// 发送到监控系统
reportSlowQuery({ queryKey, duration })
},
})
if (isLoading) {
return <LoadingSkeleton />
}
if (isError) {
return (
<ErrorDisplay
message={error.message}
onRetry={() => refetch()}
/>
)
}
return (
<div>
<UserCard user={user} />
{/* 开发环境显示调试信息 */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-4 p-2 bg-gray-100 text-xs">
<p>查询耗时: {lastQueryDuration}ms</p>
<p>刷新次数: {refetchCount}</p>
</div>
)}
</div>
)
}现在你已经掌握了增强查询的使用。但你可能注意到,我们一直在手写 queryKey,比如 ['user', userId]。随着项目变大,管理这些 key 会变得困难。接下来,让我们学习如何优雅地管理 Query Key。
6. 第四步:管理 Query Key
Query Key 是 TanStack Query 的核心概念。好的 Key 管理策略可以让你的代码更易维护、更不容易出错。
6.1 为什么需要管理 Query Key?
考虑以下场景:
// 组件 A
useQuery({ queryKey: ['user', userId], ... })
// 组件 B
useQuery({ queryKey: ['users', userId], ... }) // 拼写错误!
// 组件 C - 需要失效用户缓存
queryClient.invalidateQueries({ queryKey: ['user', userId] })问题:
- 拼写错误导致缓存不共享
- 修改 key 结构时需要全局搜索替换
- 没有类型提示
6.2 使用内置的 Key 工厂
本库提供了一套预定义的 Key 工厂:
import { queryKeys } from '@qiaopeng/tanstack-query-plus/core'
// 用户相关
queryKeys.users() // ['tanstack-query', 'users']
queryKeys.user('123') // ['tanstack-query', 'users', '123']
queryKeys.userProfile('123') // ['tanstack-query', 'users', '123', 'profile']
queryKeys.userSettings('123') // ['tanstack-query', 'users', '123', 'settings']
queryKeys.usersByRole('admin') // ['tanstack-query', 'users', 'by-role', 'admin']
// 文章相关
queryKeys.posts() // ['tanstack-query', 'posts']
queryKeys.post('456') // ['tanstack-query', 'posts', '456']
queryKeys.postsByUser('123') // ['tanstack-query', 'posts', 'by-user', '123']
queryKeys.postComments('456') // ['tanstack-query', 'posts', '456', 'comments']
// 搜索
queryKeys.search('react', 'posts') // ['tanstack-query', 'search', { query: 'react', type: 'posts' }]
// 通知
queryKeys.notifications() // ['tanstack-query', 'notifications']
queryKeys.unreadNotifications() // ['tanstack-query', 'notifications', 'unread']使用示例:
import { queryKeys } from '@qiaopeng/tanstack-query-plus/core'
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
const { data } = useEnhancedQuery({
queryKey: queryKeys.user(userId), // 类型安全,不会拼错
queryFn: () => fetchUser(userId),
})
// ...
}
// 失效缓存时也使用同样的 key
function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onSuccess: (_, { userId }) => {
// 失效该用户的所有相关缓存
queryClient.invalidateQueries({ queryKey: queryKeys.user(userId) })
}
})
}6.3 创建自定义域 Key 工厂
对于内置 Key 工厂没有覆盖的业务领域,可以创建自定义工厂:
import { createDomainKeyFactory } from '@qiaopeng/tanstack-query-plus/core'
// 创建产品域的 Key 工厂
const productKeys = createDomainKeyFactory('products')
productKeys.all() // ['tanstack-query', 'products']
productKeys.lists() // ['tanstack-query', 'products', 'list']
productKeys.list({ page: 1 }) // ['tanstack-query', 'products', 'list', { page: 1 }]
productKeys.details() // ['tanstack-query', 'products', 'detail']
productKeys.detail('abc') // ['tanstack-query', 'products', 'detail', 'abc']
productKeys.subResource('abc', 'reviews') // ['tanstack-query', 'products', 'detail', 'abc', 'reviews']
productKeys.byRelation('category', 'electronics') // ['tanstack-query', 'products', 'by-category', 'electronics']实际项目中的组织方式:
// src/queries/keys.ts
import { createDomainKeyFactory } from '@qiaopeng/tanstack-query-plus/core'
export const productKeys = createDomainKeyFactory('products')
export const orderKeys = createDomainKeyFactory('orders')
export const cartKeys = createDomainKeyFactory('cart')
export const reviewKeys = createDomainKeyFactory('reviews')
// 使用
import { productKeys } from '@/queries/keys'
useQuery({
queryKey: productKeys.detail(productId),
queryFn: () => fetchProduct(productId),
})6.4 高级 Key 工具函数
本库还提供了一些高级的 Key 工具函数:
import {
createFilteredKey,
createPaginatedKey,
createSortedKey,
createSearchKey,
createComplexKey,
matchesKeyPattern,
areKeysEqual
} from '@qiaopeng/tanstack-query-plus/core'
// 带筛选的 Key
const filteredKey = createFilteredKey(
productKeys.lists(),
{ category: 'electronics', inStock: true }
)
// ['tanstack-query', 'products', 'list', 'filtered', { category: 'electronics', inStock: true }]
// 带分页的 Key
const paginatedKey = createPaginatedKey(productKeys.lists(), 1, 20)
// ['tanstack-query', 'products', 'list', 'paginated', { page: 1, pageSize: 20 }]
// 带排序的 Key
const sortedKey = createSortedKey(productKeys.lists(), 'price', 'desc')
// ['tanstack-query', 'products', 'list', 'sorted', { sortBy: 'price', sortOrder: 'desc' }]
// 复杂查询 Key(组合多个条件)
const complexKey = createComplexKey(productKeys.lists(), {
page: 1,
pageSize: 20,
filters: { category: 'electronics' },
sortBy: 'price',
sortOrder: 'desc',
search: 'phone'
})
// 检查 Key 是否匹配模式
const matches = matchesKeyPattern(
['tanstack-query', 'products', 'detail', '123'],
['tanstack-query', 'products'] // 模式
)
// true - 可用于批量失效
// 比较两个 Key 是否相等
const equal = areKeysEqual(key1, key2)6.5 Mutation Key 工厂
除了查询 Key,mutation 也可以有 Key(用于去重、追踪等):
import { createMutationKeyFactory } from '@qiaopeng/tanstack-query-plus/core'
const productMutations = createMutationKeyFactory('products')
productMutations.create() // ['products', 'create']
productMutations.update('123') // ['products', 'update', '123']
productMutations.delete('123') // ['products', 'delete', '123']
productMutations.batch('archive') // ['products', 'batch', 'archive']现在你已经掌握了 Query Key 的管理。接下来,让我们学习如何进行数据变更(Mutation)以及如何实现乐观更新。
7. 第五步:数据变更与乐观更新
查询(Query)用于获取数据,而变更(Mutation)用于创建、更新或删除数据。本库的 useMutation 提供了内置的乐观更新支持,让用户体验更流畅。
7.1 基础 Mutation
最基本的 mutation 使用:
import { useMutation, useQueryClient } from '@qiaopeng/tanstack-query-plus'
function UpdateUserButton({ userId }) {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newName) =>
fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify({ name: newName })
}).then(r => r.json()),
onSuccess: () => {
// 成功后刷新用户数据
queryClient.invalidateQueries({ queryKey: ['user', userId] })
},
onError: (error) => {
alert(`更新失败: ${error.message}`)
}
})
return (
<button
onClick={() => mutation.mutate('新名字')}
disabled={mutation.isPending}
>
{mutation.isPending ? '更新中...' : '更新名字'}
</button>
)
}7.2 什么是乐观更新?
传统流程:
- 用户点击"更新"
- 显示 loading
- 等待服务器响应
- 更新 UI
乐观更新流程:
- 用户点击"更新"
- 立即更新 UI(假设会成功)
- 后台发送请求
- 如果失败,回滚到之前的状态
乐观更新让用户感觉应用响应更快,体验更好。
7.3 使用内置乐观更新
本库的 useMutation 内置了乐观更新支持,无需手写复杂的 onMutate/onError 逻辑:
import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
function UpdateUserName({ userId, currentName }) {
const mutation = useMutation({
mutationFn: (newName) => updateUserAPI(userId, { name: newName }),
// 乐观更新配置
optimistic: {
queryKey: ['user', userId], // 要更新的缓存 key
// 更新函数:接收旧数据和变量,返回新数据
updater: (oldData, newName) => ({
...oldData,
name: newName
}),
// 回滚回调(可选):失败时执行
rollback: (previousData, error) => {
console.error('更新失败,已回滚:', error.message)
toast.error(`更新失败: ${error.message}`)
}
},
// 标准回调仍然可用
onSuccess: () => {
toast.success('更新成功')
}
})
return (
<button onClick={() => mutation.mutate('新名字')}>
更新名字
</button>
)
}工作原理:
调用
mutation.mutate('新名字')时:- 取消该 queryKey 的进行中请求
- 保存当前缓存数据(用于回滚)
- 调用
updater立即更新缓存 - 发送实际请求
如果请求成功:
- 自动失效该 queryKey,触发重新获取最新数据
- 调用
onSuccess回调
如果请求失败:
- 自动回滚到之前的数据
- 调用
rollback回调 - 调用
onError回调
7.4 字段映射
有时候 mutation 的变量名和缓存数据的字段名不一致,可以使用字段映射:
const mutation = useMutation({
mutationFn: ({ newTitle }) => updateTodo(todoId, { title: newTitle }),
optimistic: {
queryKey: ['todo', todoId],
updater: (oldData, variables) => ({
...oldData,
...variables // 映射后的变量会自动应用
}),
// 将 mutation 变量的 newTitle 映射到缓存数据的 title
fieldMapping: {
'newTitle': 'title'
}
}
})
// 调用时
mutation.mutate({ newTitle: '新标题' })
// 缓存会更新 title 字段7.5 条件性乐观更新
本库未提供单独的 useConditionalOptimisticMutation。如需按条件启用乐观更新,使用以下两种安全模式:
- 使用两个 mutation,根据条件选择调用哪个(最清晰、类型安全):
import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
const optimisticUpdate = useMutation({
mutationFn: updateTodo,
optimistic: {
queryKey: ['todos'],
updater: (oldTodos, updatedTodo) => oldTodos?.map(t => t.id === updatedTodo.id ? { ...t, ...updatedTodo } : t)
}
})
const plainUpdate = useMutation({ mutationFn: updateTodo })
function save(todo) {
const shouldOptimistic = todo.priority === 'high'
const runner = shouldOptimistic ? optimisticUpdate : plainUpdate
runner.mutate(todo)
}- 基于状态切换
optimistic.enabled(适合全局开关):
// 以应用自身配置或组件状态为准(此处仅示例)
const enableOptimistic = true
const mutation = useMutation({
mutationFn: updateTodo,
optimistic: {
queryKey: ['todos'],
enabled: enableOptimistic,
updater: (oldTodos, updatedTodo) => oldTodos?.map(t => t.id === updatedTodo.id ? { ...t, ...updatedTodo } : t)
}
})7.6 列表操作的简化 Mutation
对于常见的列表 CRUD 操作,可以使用 useListMutation:
import { useListMutation } from '@qiaopeng/tanstack-query-plus/hooks'
function TodoList() {
const mutation = useListMutation(
async ({ operation, data }) => {
switch (operation) {
case 'create':
return api.createTodo(data)
case 'update':
return api.updateTodo(data.id, data)
case 'delete':
return api.deleteTodo(data.id)
}
},
['todos'] // 操作完成后自动失效这个 queryKey
)
const handleCreate = () => {
mutation.mutate({
operation: 'create',
data: { title: '新任务', done: false }
})
}
const handleUpdate = (todo) => {
mutation.mutate({
operation: 'update',
data: { ...todo, done: !todo.done }
})
}
const handleDelete = (todoId) => {
mutation.mutate({
operation: 'delete',
data: { id: todoId }
})
}
// ...
}7.7 批量 Mutation
本库未提供 useBatchMutation。进行批量操作时,推荐两种模式:
- 在一个 mutation 中封装批量逻辑(一次请求或并发 Promise):
import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
const batchDelete = useMutation({
mutationFn: async (ids: string[]) => {
return Promise.all(ids.map(id => api.deleteTodo(id)))
},
optimistic: {
queryKey: ['todos'],
updater: (old, ids: string[]) => old?.filter(t => !ids.includes(String(t.id)))
}
})
// 使用
batchDelete.mutate(['id1', 'id2', 'id3'])- 使用离线队列在恢复网络后批量执行(稳健且可持久化):
import { createOfflineQueueManager, mutationRegistry, serializeMutationKey } from '@qiaopeng/tanstack-query-plus/features'
import { MutationOperationType } from '@qiaopeng/tanstack-query-plus/types'
const queue = createOfflineQueueManager({ storageKey: 'todo-ops', concurrency: 3 })
async function registerDelete(id: string) {
const key = serializeMutationKey(['todos', 'delete', id])
mutationRegistry.register(key, () => api.deleteTodo(id))
await queue.add({
type: MutationOperationType.DELETE,
mutationKey: ['todos', 'delete', id],
variables: { id },
mutationFn: () => api.deleteTodo(id),
priority: 1
})
}7.8 乐观更新工具函数
本库还提供了一些工具函数来简化列表的乐观更新:
import {
listUpdater,
createAddItemConfig,
createUpdateItemConfig,
createRemoveItemConfig,
batchUpdateItems,
batchRemoveItems,
reorderItems,
conditionalUpdateItems
} from '@qiaopeng/tanstack-query-plus/utils'
// 列表更新器(要求列表项有 id 字段)
const list1 = listUpdater.add(items, newItem) // 添加到头部(如果 id 已存在则更新)
const list2 = listUpdater.update(items, { id: '1', title: '新标题' }) // 更新项
const list3 = listUpdater.remove(items, '1') // 按 id 移除项
// 创建预配置的乐观更新配置(返回 { queryKey, updater, rollback?, enabled } 对象)
const addConfig = createAddItemConfig(['todos'], {
addToTop: true, // 默认 true,添加到头部
onRollback: (error) => console.error('添加失败:', error)
})
const updateConfig = createUpdateItemConfig(['todos'])
const removeConfig = createRemoveItemConfig(['todos'])
// 在 mutation 中使用这些配置
const addMutation = useMutation({
mutationFn: createTodo,
optimistic: addConfig, // 直接使用预配置
})
// 批量更新(每个更新对象必须包含 id)
const list4 = batchUpdateItems(items, [
{ id: '1', done: true },
{ id: '2', done: true }
])
// 批量移除
const list5 = batchRemoveItems(items, ['1', '2', '3'])
// 重新排序(将 fromIndex 位置的项移动到 toIndex)
const list6 = reorderItems(items, 0, 2) // 将第一项移到第三位
// 条件更新(满足条件的项才更新)
const list7 = conditionalUpdateItems(
items,
(item) => item.status === 'pending', // 条件
(item) => ({ status: 'completed' }) // 更新内容
)7.9 完整示例:Todo 应用
import { useEnhancedQuery, useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
import { listUpdater } from '@qiaopeng/tanstack-query-plus/utils'
function TodoApp() {
// 查询 todos
const { data: todos, isLoading } = useEnhancedQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 添加 todo(乐观更新)
const addMutation = useMutation({
mutationFn: (title) => api.createTodo({ title, done: false }),
optimistic: {
queryKey: ['todos'],
updater: (oldTodos, title) => [
{ id: `temp-${Date.now()}`, title, done: false },
...(oldTodos || [])
],
rollback: (_, error) => toast.error(`添加失败: ${error.message}`)
}
})
// 切换完成状态(乐观更新)
const toggleMutation = useMutation({
mutationFn: (todo) => api.updateTodo(todo.id, { done: !todo.done }),
optimistic: {
queryKey: ['todos'],
updater: (oldTodos, todo) =>
oldTodos?.map(t => t.id === todo.id ? { ...t, done: !t.done } : t),
}
})
// 删除 todo(乐观更新)
const deleteMutation = useMutation({
mutationFn: (todoId) => api.deleteTodo(todoId),
optimistic: {
queryKey: ['todos'],
updater: (oldTodos, todoId) => oldTodos?.filter(t => t.id !== todoId),
}
})
if (isLoading) return <div>加载中...</div>
return (
<div>
<AddTodoForm onAdd={(title) => addMutation.mutate(title)} />
<ul>
{todos?.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleMutation.mutate(todo)}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button onClick={() => deleteMutation.mutate(todo.id)}>
删除
</button>
</li>
))}
</ul>
</div>
)
}
}7.10 安全提示
- 明确回滚路径:在
onError或rollback中恢复缓存或触发重新拉取 - 稳定的
queryKey:使用 Key 工厂,避免结构漂移导致更新不到位 - 变量安全:Mutation 变量不包含敏感信息(如 token),错误上报需脱敏
- 冲突处理:对 409 触发家族失效与 UI 提示;对 500 展示兜底提示并记录错误
现在你已经掌握了数据变更和乐观更新。接下来,让我们学习如何处理无限滚动和分页场景。
8. 第六步:无限滚动与分页
无限滚动是现代应用中常见的交互模式。本库提供了 useEnhancedInfiniteQuery 和多种分页模式的工厂函数,让实现变得简单。
8.1 理解三种分页模式
在实际项目中,后端 API 通常采用以下三种分页方式之一:
游标分页(Cursor Pagination)
- 使用游标(通常是最后一条记录的 ID)来获取下一页
- 适合:社交媒体 feed、聊天记录
- 示例:
/api/posts?cursor=abc123
偏移分页(Offset Pagination)
- 使用 offset 和 limit 来获取数据
- 适合:传统列表、搜索结果
- 示例:
/api/posts?offset=20&limit=10
页码分页(Page Number Pagination)
- 使用页码来获取数据
- 适合:传统分页 UI
- 示例:
/api/posts?page=2
8.2 游标分页
import {
useEnhancedInfiniteQuery,
createCursorPaginationOptions
} from '@qiaopeng/tanstack-query-plus/hooks'
// 假设 API 返回格式:
// { items: [...], cursor: 'next-cursor' | null }
function PostFeed() {
// 创建游标分页配置
const options = createCursorPaginationOptions({
queryKey: ['posts', 'feed'],
queryFn: async (cursor) => {
const url = cursor
? `/api/posts?cursor=${cursor}`
: '/api/posts'
const response = await fetch(url)
return response.json()
// 返回 { items: Post[], cursor: string | null }
},
initialCursor: null, // 初始游标
staleTime: 30000,
})
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useEnhancedInfiniteQuery(options)
if (isLoading) return <div>加载中...</div>
return (
<div>
{/* 展平所有页的数据 */}
{data?.pages.map((page, pageIndex) => (
<div key={pageIndex}></div> {page.items.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
{/* 加载更多按钮 */}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? '加载中...'
: hasNextPage
? '加载更多'
: '没有更多了'}
</button>
</div>
)
}8.3 偏移分页
import {
useEnhancedInfiniteQuery,
createOffsetPaginationOptions
} from '@qiaopeng/tanstack-query-plus/hooks'
// 假设 API 返回格式:
// { items: [...], total: 100, hasMore: true }
function ProductList() {
const options = createOffsetPaginationOptions({
queryKey: ['products'],
queryFn: async (offset, limit) => {
const response = await fetch(
`/api/products?offset=${offset}&limit=${limit}`
)
return response.json()
// 返回 { items: Product[], total: number, hasMore: boolean }
},
limit: 20, // 每页数量
})
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useEnhancedInfiniteQuery(options)
// 计算已加载的总数
const loadedCount = data?.pages.reduce(
(sum, page) => sum + page.items.length,
0
) || 0
return (
<div>
<div className="grid grid-cols-4 gap-4">
{data?.pages.flatMap(page => page.items).map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
<div className="mt-4 text-center">
<p>已加载 {loadedCount} / {data?.pages[0]?.total || 0} 个商品</p>
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
)}
</div>
</div>
)
}8.4 页码分页
import {
useEnhancedInfiniteQuery,
createPageNumberPaginationOptions
} from '@qiaopeng/tanstack-query-plus/hooks'
// 假设 API 返回格式:
// { items: [...], page: 1, totalPages: 10 }
function ArticleList() {
const options = createPageNumberPaginationOptions({
queryKey: ['articles'],
queryFn: async (page) => {
const response = await fetch(`/api/articles?page=${page}`)
return response.json()
// 返回 { items: Article[], page: number, totalPages: number }
},
})
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
} = useEnhancedInfiniteQuery(options)
const currentPage = data?.pages.length || 0
const totalPages = data?.pages[0]?.totalPages || 0
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.items.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
))}
<div className="flex justify-between mt-4">
<button
onClick={() => fetchPreviousPage()}
disabled={!hasPreviousPage}
>
上一页
</button>
<span>第 {currentPage} / {totalPages} 页</span>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : '下一页'}
</button>
</div>
</div>
)
}8.5 无限滚动(自动加载)
结合 Intersection Observer 实现滚动到底部自动加载:
import { useRef, useEffect } from 'react'
import { useEnhancedInfiniteQuery, createOffsetPaginationOptions } from '@qiaopeng/tanstack-query-plus/hooks'
function InfiniteScrollList() {
const loadMoreRef = useRef(null)
const options = createOffsetPaginationOptions({
queryKey: ['items'],
queryFn: (offset, limit) => fetchItems(offset, limit),
limit: 20,
})
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useEnhancedInfiniteQuery(options)
// 监听滚动到底部
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current)
}
return () => observer.disconnect()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
return (
<div>
{data?.pages.flatMap(page => page.items).map(item => (
<ItemCard key={item.id} item={item} />
))}
{/* 触发加载的哨兵元素 */}
<div ref={loadMoreRef} className="h-10">
{isFetchingNextPage && <div>加载中...</div>}
{!hasNextPage && <div>已经到底了</div>}
</div>
</div>
)
}8.6 自定义无限查询配置
如果预设的分页模式不满足需求,可以使用 createInfiniteQueryOptions 创建自定义配置:
import { createInfiniteQueryOptions, useEnhancedInfiniteQuery } from '@qiaopeng/tanstack-query-plus/hooks'
// 使用 createInfiniteQueryOptions 创建自定义分页配置
const customOptions = createInfiniteQueryOptions({
queryKey: ['custom-list'],
queryFn: ({ pageParam }) => fetchCustomData(pageParam),
initialPageParam: { page: 1, filter: 'active' },
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.hasMore) {
return { ...lastPageParam, page: lastPageParam.page + 1 }
}
return undefined // 没有更多数据
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam.page > 1) {
return { ...firstPageParam, page: firstPageParam.page - 1 }
}
return undefined
},
staleTime: 60000,
gcTime: 300000,
})
const result = useEnhancedInfiniteQuery(customOptions)方式二:也可以直接传递配置给 useEnhancedInfiniteQuery:
const result = useEnhancedInfiniteQuery({
queryKey: ['custom-list'],
queryFn: ({ pageParam }) => fetchCustomData(pageParam),
initialPageParam: { page: 1, filter: 'active' },
getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextPage : undefined,
})方式三:使用 TanStack Query 的 infiniteQueryOptions(如果你需要与原生 API 保持一致):
import { infiniteQueryOptions } from '@tanstack/react-query'
const customOptions = infiniteQueryOptions({
queryKey: ['custom-list'],
queryFn: ({ pageParam }) => fetchCustomData(pageParam),
initialPageParam: { page: 1, filter: 'active' },
getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextPage : undefined,
})
const result = useEnhancedInfiniteQuery(customOptions)现在你已经掌握了无限滚动和分页。在复杂的应用中,我们经常需要同时发起多个查询。接下来,让我们学习批量查询。
9. 第七步:全局状态与 Mutation 监控
在复杂的应用中,你可能需要监控全局的加载状态,或者获取正在进行的 Mutation 进度。本库补全了 v5 的状态监控 Hooks。
9.1 监控全局加载状态
使用 useIsFetching 和 useIsMutating 可以实时感知后台活动:
import { useIsFetching, useIsMutating } from '@qiaopeng/tanstack-query-plus/hooks'
function GlobalLoadingIndicator() {
const isFetching = useIsFetching() // 正在进行的 Query 数量
const isMutating = useIsMutating() // 正在进行的 Mutation 数量
if (!isFetching && !isMutating) return null
return (
<div className="fixed top-0 right-0 p-4">
{isFetching > 0 && <span>数据加载中...</span>}
{isMutating > 0 && <span>后台同步中...</span>}
</div>
)
}9.2 监控 Mutation 详细状态
useMutationState 允许你订阅 Mutation 缓存,获取特定任务的进度或结果:
import { useMutationState } from '@qiaopeng/tanstack-query-plus/hooks'
function UploadManager() {
// 获取所有 ['upload'] 相关的 mutation 状态
const uploads = useMutationState({
filters: { mutationKey: ['upload'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<div>
<h3>正在上传 ({uploads.length})</h3>
<ul>
{uploads.map((file, i) => (
<li key={i}>{file.name} 正在传输...</li>
))}
</ul>
</div>
)
}10. 第八步:批量查询与仪表盘
在仪表盘、数据概览等场景中,我们经常需要同时发起多个查询。本库提供了强大的批量查询功能,包括统计信息、批量操作和错误聚合。
9.1 基础批量查询
使用 useEnhancedQueries 同时发起多个查询:
import { useEnhancedQueries, batchQueryUtils } from '@qiaopeng/tanstack-query-plus/hooks'
function Dashboard() {
const { data: results, stats, operations } = useEnhancedQueries([
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['comments'], queryFn: fetchComments },
{ queryKey: ['analytics'], queryFn: fetchAnalytics },
])
// stats 包含聚合统计信息
// {
// total: 4, // 总查询数
// loading: 1, // 加载中的数量
// success: 2, // 成功的数量
// error: 1, // 失败的数量
// stale: 0, // 过期的数量
// successRate: 50, // 成功率 (%)
// errorRate: 25, // 错误率 (%)
// }
return (
<div>
{/* 显示加载状态 */}
<div className="mb-4 p-4 bg-gray-100 rounded">
<p>加载进度: {stats.success}/{stats.total}</p>
<p>成功率: {stats.successRate.toFixed(1)}%</p>
{stats.loading > 0 && <p>正在加载 {stats.loading} 个查询...</p>}
</div>
{/* 批量操作按钮 */}
<div className="space-x-2 mb-4">
<button onClick={() => operations.refetchAll()}>
刷新全部
</button>
<button onClick={() => operations.invalidateAll()}>
失效全部
</button>
<button onClick={() => operations.cancelAll()}>
取消全部
</button>
</div>
{/* 错误处理 */}
{batchQueryUtils.hasError(results) && (
<div className="p-4 bg-red-100 rounded mb-4">
<p>部分查询失败</p>
<button onClick={() => operations.retryFailed()}>
重试失败的查询
</button>
</div>
)}
{/* 数据展示 */}
{batchQueryUtils.isAllSuccess(results) && (
<div className="grid grid-cols-2 gap-4">
<UserStats data={results[0].data} />
<PostStats data={results[1].data} />
<CommentStats data={results[2].data} />
<AnalyticsChart data={results[3].data} />
</div>
)}
</div>
)
}9.2 批量查询工具函数
batchQueryUtils 提供了丰富的工具函数:
import { batchQueryUtils } from '@qiaopeng/tanstack-query-plus/hooks'
// 状态检查
batchQueryUtils.isAllLoading(results) // 是否全部加载中
batchQueryUtils.isAllSuccess(results) // 是否全部成功
batchQueryUtils.isAllPending(results) // 是否全部待处理
batchQueryUtils.hasError(results) // 是否有错误
batchQueryUtils.hasStale(results) // 是否有过期数据
batchQueryUtils.isAnyFetching(results) // 是否有正在获取的
// 数据提取
batchQueryUtils.getAllData(results) // 获取所有成功的数据
batchQueryUtils.getSuccessData(results) // 获取成功数据(带类型)
batchQueryUtils.getAllErrors(results) // 获取所有错误
batchQueryUtils.getFirstError(results) // 获取第一个错误
// 高级功能
batchQueryUtils.createErrorAggregate(results, queries) // 创建错误聚合
batchQueryUtils.createOperationReport(results, queries, startTime) // 创建操作报告9.3 仪表盘查询(命名数据)
useDashboardQueries 让你可以用对象形式定义查询,返回命名的数据:
import { useDashboardQueries } from '@qiaopeng/tanstack-query-plus/hooks'
function AdminDashboard() {
const {
data, // 命名的数据对象
isLoading, // 任一查询加载中
isError, // 任一查询出错
isSuccess, // 全部成功
stats, // 统计信息
results // 原始结果数组
} = useDashboardQueries({
users: {
queryKey: ['dashboard', 'users'],
queryFn: fetchUserStats
},
revenue: {
queryKey: ['dashboard', 'revenue'],
queryFn: fetchRevenueStats
},
orders: {
queryKey: ['dashboard', 'orders'],
queryFn: fetchOrderStats
},
traffic: {
queryKey: ['dashboard', 'traffic'],
queryFn: fetchTrafficStats
},
})
if (isLoading) return <DashboardSkeleton />
if (isError) return <DashboardError />
// 直接通过名称访问数据
return (
<div className="grid grid-cols-2 gap-6">
<StatCard title="用户" value={data.users?.total} />
<StatCard title="收入" value={data.revenue?.total} />
<StatCard title="订单" value={data.orders?.count} />
<TrafficChart data={data.traffic} />
</div>
)
}9.4 依赖查询链
有时候后续查询依赖于前一个查询的结果。使用 useDependentBatchQueries:
import { useDependentBatchQueries } from '@qiaopeng/tanstack-query-plus/hooks'
function UserDashboard({ userId }) {
const {
primaryResult, // 主查询结果
results, // 从查询结果数组
stats, // 统计信息
operations // 批量操作
} = useDependentBatchQueries({
// 主查询:获取用户信息
primaryQuery: {
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
},
// 从查询:基于用户信息获取相关数据
dependentQueries: (user) => [
{
queryKey: ['posts', user.id],
queryFn: () => fetchUserPosts(user.id)
},
{
queryKey: ['followers', user.id],
queryFn: () => fetchFollowers(user.id)
},
{
queryKey: ['following', user.id],
queryFn: () => fetchFollowing(user.id)
},
// 可以使用用户数据中的任何信息
...(user.isAdmin ? [
{
queryKey: ['admin-stats'],
queryFn: fetchAdminStats
}
] : [])
],
})
if (primaryResult.isLoading) return <div>加载用户信息...</div>
if (primaryResult.isError) return <div>加载失败</div>
const user = primaryResult.data
const [postsResult, followersResult, followingResult] = results
return (
<div>
<UserHeader user={user} />
<div className="grid grid-cols-3 gap-4">
<PostList
posts={postsResult?.data}
isLoading={postsResult?.isLoading}
/>
<FollowerList
followers={followersResult?.data}
isLoading={followersResult?.isLoading}
/>
<FollowingList
following={followingResult?.data}
isLoading={followingResult?.isLoading}
/>
</div>
</div>
)
}9.5 动态批量查询
当查询数量是动态的(比如基于一个 ID 列表):
import { useDynamicBatchQueries } from '@qiaopeng/tanstack-query-plus/hooks'
function ProductComparison({ productIds }) {
const { data: results, stats } = useDynamicBatchQueries({
items: productIds, // 动态的 ID 列表
queryKeyPrefix: ['product'],
queryFn: (productId) => fetchProduct(productId),
enabled: productIds.length > 0,
staleTime: 60000,
})
if (stats.loading > 0) {
return <div>加载中... ({stats.success}/{stats.total})</div>
}
const products = batchQueryUtils.getSuccessData(results)
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}9.6 自动刷新批量查询
对于需要定期刷新的仪表盘:
import { useAutoRefreshBatchQueries } from '@qiaopeng/tanstack-query-plus/hooks'
function LiveDashboard() {
const { data: results, stats } = useAutoRefreshBatchQueries({
queries: [
{ queryKey: ['live-users'], queryFn: fetchLiveUsers },
{ queryKey: ['live-orders'], queryFn: fetchLiveOrders },
{ queryKey: ['live-revenue'], queryFn: fetchLiveRevenue },
],
refreshInterval: 30000, // 每 30 秒刷新
enabled: true,
})
// ...
}9.7 条件批量查询
只执行满足条件的查询:
import { useConditionalBatchQueries } from '@qiaopeng/tanstack-query-plus/hooks'
function ConditionalDashboard({ userRole }) {
const { data: results } = useConditionalBatchQueries([
{
queryKey: ['basic-stats'],
queryFn: fetchBasicStats,
enabled: true // 总是执行
},
{
queryKey: ['admin-stats'],
queryFn: fetchAdminStats,
enabled: userRole === 'admin' // 只有管理员执行
},
{
queryKey: ['premium-stats'],
queryFn: fetchPremiumStats,
enabled: userRole === 'premium' || userRole === 'admin'
},
])
// ...
}现在你已经掌握了批量查询。为了提升用户体验,我们可以在用户需要数据之前就预先获取。接下来,让我们学习智能预取。
10. 第八步:智能预取
预取(Prefetch)是指在用户实际需要数据之前就提前获取。这可以显著提升用户体验,让页面切换感觉更快。本库提供了多种预取策略。
10.1 悬停预取
当用户将鼠标悬停在链接上时预取数据:
import { useHoverPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function UserLink({ userId, userName }) {
// 返回需要绑定到元素的事件处理器
const hoverProps = useHoverPrefetch(
['user', userId], // queryKey
() => fetchUser(userId), // queryFn
{
hoverDelay: 200, // 悬停 200ms 后开始预取(避免快速划过触发)
minInterval: 1000, // 同一个 key 最小预取间隔
staleTime: 30000, // 数据新鲜时不预取
}
)
return (
<a
href={`/user/${userId}`}
{...hoverProps} // 绑定 onMouseEnter, onMouseLeave, onFocus
>
{userName}
</a>
)
}工作原理:
- 用户鼠标移入元素
- 等待
hoverDelay毫秒 - 检查数据是否已缓存且新鲜
- 如果需要,发起预取请求
- 用户点击链接时,数据已经准备好了
10.2 智能预取
useSmartPrefetch 会自动检测网络状态,在慢网络时跳过预取:
import { useSmartPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function ProductCard({ productId }) {
const { prefetch, shouldPrefetch, clearPrefetchHistory } = useSmartPrefetch()
const handleMouseEnter = () => {
// 自动检测网络状态,慢网络时不预取
prefetch(
['product', productId],
() => fetchProduct(productId),
{ staleTime: 60000 }
)
}
return (
<div
onMouseEnter={handleMouseEnter}
className="product-card"
>
<ProductImage id={productId} />
<ProductInfo id={productId} />
{/* 可选:显示网络状态 */}
{!shouldPrefetch && (
<span className="text-xs text-gray-400">
慢网络,已禁用预取
</span>
)}
</div>
)
}10.3 视口预取
当元素进入视口时预取(需要安装 react-intersection-observer):
import { useInViewPrefetch } from '@qiaopeng/tanstack-query-plus/hooks/inview'
function LazySection({ sectionId }) {
// 返回一个 ref,绑定到需要监听的元素
const ref = useInViewPrefetch(
['section', sectionId],
() => fetchSectionData(sectionId),
{
threshold: 0.1, // 10% 可见时触发
rootMargin: '100px', // 提前 100px 触发(元素还没完全进入视口)
triggerOnce: true, // 只触发一次
}
)
return (
<section ref={ref}>
<SectionContent id={sectionId} />
</section>
)
}使用场景:
- 长页面的各个区块
- 图片懒加载
- 无限滚动列表的下一批数据
10.4 路由预取
在路由切换前预取下一个页面的数据:
import { useRoutePrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
import { Link, useNavigate } from 'react-router-dom'
function Navigation() {
const prefetch = useRoutePrefetch()
const navigate = useNavigate()
const handlePrefetchUser = (userId) => {
prefetch(
['user', userId],
() => fetchUser(userId),
{ staleTime: 30000 }
)
}
return (
<nav>
<Link
to="/user/123"
onMouseEnter={() => handlePrefetchUser('123')}
>
用户 123
</Link>
{/* 或者在按钮点击前预取 */}
<button
onMouseEnter={() => handlePrefetchUser('456')}
onClick={() => navigate('/user/456')}
>
查看用户 456
</button>
</nav>
)
}10.5 条件预取
只在满足条件时预取:
import { useConditionalPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function SearchResults({ query, isHovered }) {
// 当 isHovered 为 true 时预取
useConditionalPrefetch(
['search', query],
() => fetchSearchResults(query),
isHovered, // 条件
{ delay: 300 } // 延迟 300ms
)
// ...
}10.6 空闲时预取
利用浏览器空闲时间预取:
import { useIdlePrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function App() {
// 在浏览器空闲时预取常用数据
useIdlePrefetch(
['common-data'],
fetchCommonData,
{
timeout: 2000, // 最多等待 2 秒进入空闲
enabled: true
}
)
return <MainContent />
}工作原理:
- 使用
requestIdleCallbackAPI - 在浏览器空闲时执行预取
- 不影响主线程性能
10.7 周期预取
定期预取数据,保持缓存新鲜:
import { usePeriodicPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function Dashboard() {
// 每分钟预取一次
usePeriodicPrefetch(
['dashboard-stats'],
fetchDashboardStats,
{
interval: 60000, // 60 秒
enabled: true
}
)
// ...
}10.8 批量预取
一次预取多个查询:
import { useBatchPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function HomePage() {
const batchPrefetch = useBatchPrefetch()
useEffect(() => {
// 页面加载后预取常用数据
batchPrefetch([
{ queryKey: ['featured-products'], queryFn: fetchFeaturedProducts },
{ queryKey: ['categories'], queryFn: fetchCategories },
{ queryKey: ['promotions'], queryFn: fetchPromotions },
])
}, [batchPrefetch])
// ...
}10.9 优先级预取
按优先级执行预取任务:
import { usePriorityPrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function App() {
const { addPrefetchTask, processTasks, taskCount } = usePriorityPrefetch()
useEffect(() => {
// 添加不同优先级的预取任务
addPrefetchTask(['critical-data'], fetchCriticalData, 'high')
addPrefetchTask(['important-data'], fetchImportantData, 'medium')
addPrefetchTask(['optional-data'], fetchOptionalData, 'low')
// 按优先级顺序执行
processTasks()
}, [])
return (
<div>
{taskCount > 0 && <span>预取中... ({taskCount} 个任务)</span>}
<MainContent />
</div>
)
}10.10 预测性预取
基于用户行为预测并预取:
import { usePredictivePrefetch } from '@qiaopeng/tanstack-query-plus/hooks'
function ProductBrowser() {
const {
recordInteraction,
getPredictions,
prefetchPredicted
} = usePredictivePrefetch()
const handleProductClick = (productId) => {
// 记录用户交互
recordInteraction('click', productId)
navigate(`/product/${productId}`)
}
const handleProductHover = (productId) => {
recordInteraction('hover', productId)
}
// 基于历史行为预取
useEffect(() => {
prefetchPredicted((target) => ({
queryKey: ['product', target],
queryFn: () => fetchProduct(target)
}))
}, [prefetchPredicted])
return (
<div>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onClick={() => handleProductClick(product.id)}
onMouseEnter={() => handleProductHover(product.id)}
/>
))}
</div>
)
}10.11 预取最佳实践
- 不要过度预取:只预取用户很可能需要的数据
- 设置合理的 staleTime:避免重复预取新鲜数据
- 考虑网络状况:使用
useSmartPrefetch在慢网络时禁用 - 使用延迟:悬停预取应该有延迟,避免快速划过触发
- 优先级管理:关键数据优先预取
现在你已经掌握了预取策略。接下来,让我们学习 Suspense 模式,它可以让你的代码更简洁。
10.12 安全提示
- 结合网络状况:使用
useSmartPrefetch在慢网络禁用预取,避免拥塞 - 控制频率与间隔:为悬停/路由预取设置
minInterval,避免重复请求 - 严格限定 Key:预取目标必须是稳定且可序列化的
queryKey - 避免敏感信息:不要将敏感数据拼入
queryKey - 可回收:在复杂页面中适时清理预取历史(
clearPrefetchHistory)以避免状态膨胀
12. 第十步:Suspense 模式与 SSR
React Suspense 提供了声明式的加载处理。本库不仅支持增强的 Suspense Hooks,还提供了完善的 SSR 水合支持。
12.1 基础 Suspense 查询
useEnhancedSuspenseQuery 保证 data 始终存在,省去非空判断:
import { useEnhancedSuspenseQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function UserProfile({ userId }) {
const { data } = useEnhancedSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
return <div>{data.name}</div>
}12.2 SSR 水合支持
在 Next.js (App Router) 或其他 SSR 框架中,使用 HydrationBoundary 进行注水:
// Server Component
import { dehydrate, QueryClient } from '@qiaopeng/tanstack-query-plus'
import { HydrationBoundary } from '@qiaopeng/tanstack-query-plus/components'
export default async function Page() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts })
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
)
}12.3 使用 SuspenseWrapper 组件
本库提供了 SuspenseWrapper 和 QuerySuspenseWrapper 组件,它们组合了 Suspense 和 ErrorBoundary:
import { SuspenseWrapper, QuerySuspenseWrapper } from '@qiaopeng/tanstack-query-plus/components'
function UserPage({ userId }) {
return (
<SuspenseWrapper
fallback={<UserSkeleton />}
errorFallback={(error, reset) => (
<div className="error-container">
<p>加载失败: {error.message}</p>
<button onClick={reset}>重试</button>
</div>
)}
onError={(error, info) => {
// 上报错误到监控系统
reportError(error, info)
}}
resetKeys={[userId]} // userId 变化时重置错误状态
>
<UserProfile userId={userId} />
</SuspenseWrapper>
)
}
// QuerySuspenseWrapper 是 SuspenseWrapper 的别名,语义更清晰
function DataPage() {
return (
<QuerySuspenseWrapper
fallback={<DataSkeleton />}
errorFallback={(error, reset) => (
<ErrorDisplay error={error} onRetry={reset} />
)}
>
<DataComponent />
</QuerySuspenseWrapper>
)
}注意:QuerySuspenseWrapper 和 SuspenseWrapper 功能完全相同,只是名称不同。使用 QuerySuspenseWrapper 可以让代码语义更清晰,表明这是用于查询的 Suspense 包装器。
11.4 QueryErrorBoundary
专门为查询设计的错误边界,集成了 React Query 的错误重置:
import { QueryErrorBoundary } from '@qiaopeng/tanstack-query-plus/components'
function DataSection() {
return (
<QueryErrorBoundary
fallback={(error, reset) => (
<div>
<p>查询失败: {error.message}</p>
<button onClick={reset}>重新加载</button>
</div>
)}
resetKeys={['data-key']}
>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</QueryErrorBoundary>
)
}11.5 Loading 组件库
本库提供了多种预设的 Loading 组件:
import {
DefaultLoadingFallback, // 默认加载指示器
SmallLoadingIndicator, // 小型加载指示器
FullScreenLoading, // 全屏加载
TextSkeletonFallback, // 文本骨架屏
CardSkeletonFallback, // 卡片骨架屏
ListSkeletonFallback, // 列表骨架屏
PageSkeletonFallback, // 页面骨架屏
} from '@qiaopeng/tanstack-query-plus/components'
// 使用示例
<SuspenseWrapper fallback={<DefaultLoadingFallback />}>
<Content />
</SuspenseWrapper>
<SuspenseWrapper fallback={<ListSkeletonFallback items={5} />}>
<UserList />
</SuspenseWrapper>
<SuspenseWrapper fallback={<CardSkeletonFallback />}>
<ProductCard />
</SuspenseWrapper>
// 小型加载指示器(用于按钮等)
<SmallLoadingIndicator size="sm" /> // sm | md | lg
// 全屏加载(用于页面切换)
<FullScreenLoading message="正在加载页面..." />
// 文本骨架屏
<TextSkeletonFallback lines={3} />11.6 Suspense 无限查询
import { useEnhancedSuspenseInfiniteQuery } from '@qiaopeng/tanstack-query-plus/hooks'
function PostList() {
const { data, fetchNextPage, hasNextPage } = useEnhancedSuspenseInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
return (
<div>
{data.pages.flatMap(page => page.items).map(post => (
<PostCard key={post.id} post={post} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>加载更多</button>
)}
</div>
)
}
// 使用
<SuspenseWrapper fallback={<PostListSkeleton />}>
<PostList />
</SuspenseWrapper>11.7 创建可复用的 Suspense 查询
使用工厂函数创建可复用的 Suspense 查询:
import { createSuspenseQuery } from '@qiaopeng/tanstack-query-plus/hooks'
// 创建一个可复用的用户查询 hook
// 参数1: queryKey 生成函数,接收变量返回 queryKey
// 参数2: queryFn,接收 QueryFunctionContext(包含 queryKey, signal 等)
// 参数3: 可选的默认配置
const useUserSuspense = createSuspenseQuery(
(userId: string) => ['user', userId],
async (context) => {
// context.queryKey 是 ['user', userId]
// context.signal 可用于取消请求
const [, userId] = context.queryKey
return fetchUser(userId as string)
},
{ staleTime: 30000 }
)
// 使用:传入变量,返回 Suspense 查询结果
function UserProfile({ userId }) {
const { data } = useUserSuspense(userId)
return <div>{data.name}</div>
}11.8 嵌套 Suspense
对于复杂页面,可以使用嵌套的 Suspense 来实现渐进式加载:
function UserDashboard({ userId }) {
return (
<div>
{/* 用户信息先加载 */}
<SuspenseWrapper fallback={<UserHeaderSkeleton />}>
<UserHeader userId={userId} />
</SuspenseWrapper>
<div className="grid grid-cols-2 gap-4">
{/* 文章列表独立加载 */}
<SuspenseWrapper fallback={<PostListSkeleton />}>
<UserPosts userId={userId} />
</SuspenseWrapper>
{/* 统计信息独立加载 */}
<SuspenseWrapper fallback={<StatsSkeleton />}>
<UserStats userId={userId} />
</SuspenseWrapper>
</div>
</div>
)
}这样,各个区块可以独立加载,用户能更快看到部分内容。
11.9 Suspense 最佳实践
- 合理划分 Suspense 边界:不要把整个页面包在一个 Suspense 里
- 使用骨架屏:比简单的 "加载中..." 体验更好
- 处理错误:始终配合 ErrorBoundary 使用
- 设置 resetKeys:确保参数变化时能正确重置状态
- 考虑 SSR:Suspense 在服务端渲染时有特殊行为
现在你已经掌握了 Suspense 模式。接下来,让我们学习如何实现离线支持和数据持久化。
12. 第十步:离线支持与持久化
现代 Web 应用需要在网络不稳定甚至离线时也能正常工作。本库提供了完整的离线支持和数据持久化功能。
12.1 启用持久化
在第 3 章我们已经介绍了如何启用持久化:
<PersistQueryClientProvider
client={queryClient}
enablePersistence // 启用 localStorage 持久化
enableOfflineSupport // 启用离线状态监听
>
<App />
</PersistQueryClientProvider>启用后:
- 查询缓存会自动保存到 localStorage
- 页面刷新后自动恢复
- 网络状态变化会自动处理
12.2 监听网络状态
使用 usePersistenceStatus hook 可以方便地监听网络状态:
import { usePersistenceStatus } from '@qiaopeng/tanstack-query-plus'
function NetworkIndicator() {
const { isOnline, isOffline } = usePersistenceStatus()
return (
<div className={`network-status ${isOffline ? 'offline' : 'online'}`}>
{isOffline ? (
<span>📴 离线模式 - 数据可能不是最新的</span>
) : (
<span>🌐 在线</span>
)}
</div>
)
}底层 API:如果你需要更细粒度的控制,也可以直接使用底层 API:
import { useState, useEffect } from 'react'
import { isOnline, subscribeToOnlineStatus } from '@qiaopeng/tanstack-query-plus/features'
function NetworkIndicator() {
const [online, setOnline] = useState(isOnline())
useEffect(() => {
const unsubscribe = subscribeToOnlineStatus(setOnline)
return unsubscribe
}, [])
return <div>{online ? '在线' : '离线'}</div>
}12.3 手动管理持久化
使用 usePersistenceManager hook 可以方便地管理缓存:
import { usePersistenceManager } from '@qiaopeng/tanstack-query-plus'
function SettingsPage() {
const { clearCache, getOnlineStatus } = usePersistenceManager()
const handleClearCache = () => {
clearCache() // 清除默认缓存
// 或指定 key: clearCache('my-cache-key')
alert('缓存已清除')
}
return (
<div>
<p>网络状态: {getOnlineStatus() ? '在线' : '离线'}</p>
<button onClick={handleClearCache}>清除缓存</button>
</div>
)
}底层 API:也可以直接使用底层函数:
import { clearCache, isOnline } from '@qiaopeng/tanstack-query-plus/features'
function SettingsPage() {
const handleClearCache = () => {
clearCache() // 清除默认缓存
alert('缓存已清除')
}
return (
<div>
<p>网络状态: {isOnline() ? '在线' : '离线'}</p>
<button onClick={handleClearCache}>清除缓存</button>
</div>
)
}12.4 离线功能 API
本库提供了丰富的离线功能 API:
import {
isOnline,
subscribeToOnlineStatus,
clearCache,
clearExpiredCache,
checkStorageSize,
getStorageStats,
} from '@qiaopeng/tanstack-query-plus/features'
// 检查网络状态
const online = isOnline()
// 订阅网络状态变化
const unsubscribe = subscribeToOnlineStatus((online) => {
console.log('网络状态:', online ? '在线' : '离线')
if (online) {
// 网络恢复,可以同步数据
syncPendingChanges()
}
})
// 清除缓存
clearCache() // 清除所有缓存
clearCache('my-cache-key') // 清除指定缓存
// 清除过期缓存
clearExpiredCache('tanstack-query-cache', 24 * 60 * 60 * 1000) // 清除超过 24 小时的缓存
// 检查存储大小
const sizeInfo = checkStorageSize()
console.log(`缓存大小: ${sizeInfo.sizeInMB}MB`)
if (sizeInfo.shouldMigrate) {
console.log('建议迁移到 IndexedDB')
}
// 获取存储统计
const stats = getStorageStats()
console.log({
exists: stats.exists,
age: stats.age, // 缓存年龄(毫秒)
queriesCount: stats.queriesCount,
mutationsCount: stats.mutationsCount,
sizeInfo: stats.sizeInfo,
})12.5 离线队列管理器
对于需要在离线时也能操作的应用,可以使用离线队列管理器:
import { createOfflineQueueManager, isOnline, mutationRegistry, serializeMutationKey } from '@qiaopeng/tanstack-query-plus/features'
import { MutationOperationType } from '@qiaopeng/tanstack-query-plus/types'
// 创建队列管理器
const queueManager = createOfflineQueueManager({
maxSize: 100, // 最大队列大小
autoExecuteInterval: 5000, // 自动执行间隔(毫秒)
executeOnReconnect: true, // 网络恢复时自动执行
operationTimeout: 30000, // 操作超时时间
concurrency: 3, // 并发执行数
})
// 注册 mutation 函数(用于恢复队列时执行)
// 注册函数签名为 () => Promise<unknown>,如需变量请使用闭包或在入队项的 mutationFn 捕获
mutationRegistry.register(serializeMutationKey(['updateUser']), () => updateUserAPI(savedUserData))
mutationRegistry.register(serializeMutationKey(['createPost']), () => createPostAPI(savedPostData))
// 添加操作到队列
async function handleUpdateUser(userData) {
if (!isOnline()) {
// 离线时添加到队列
await queueManager.add({
type: MutationOperationType.UPDATE,
mutationKey: ['updateUser'],
variables: userData,
mutationFn: () => updateUserAPI(userData),
priority: 1, // 优先级(数字越大越优先)
})
toast.info('已保存到离线队列,网络恢复后将自动同步')
} else {
// 在线时直接执行
await updateUserAPI(userData)
}
}
### 队列管理器操作参考
// 获取队列状态
const state = queueManager.getState()
console.log({
isOffline: state.isOffline,
queuedOperations: state.queuedOperations,
failedQueries: state.failedQueries,
isRecovering: state.isRecovering,
})
// 手动执行队列
const result = await queueManager.execute()
console.log(`成功: ${result.success}, 失败: ${result.failed}, 跳过: ${result.skipped}`)
// 获取队列中的操作
const operations = queueManager.getOperations()
// 清空队列
await queueManager.clear()
// 销毁管理器(清理定时器和监听器)
queueManager.destroy()12.6 完整的离线应用示例
import { useState, useEffect } from 'react'
import { createOfflineQueueManager } from '@qiaopeng/tanstack-query-plus/features'
import { MutationOperationType } from '@qiaopeng/tanstack-query-plus/types'
import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
import { useQueryClient, usePersistenceStatus } from '@qiaopeng/tanstack-query-plus'
// 创建全局队列管理器
const offlineQueue = createOfflineQueueManager({
executeOnReconnect: true,
autoExecuteInterval: 10000,
})
function TodoApp() {
const queryClient = useQueryClient()
const { isOnline: networkStatus } = usePersistenceStatus() // 使用 hook 监听网络状态
const [pendingCount, setPendingCount] = useState(0)
// 网络状态变化时显示提示
useEffect(() => {
if (networkStatus) {
toast.success('网络已恢复,正在同步数据...')
} else {
toast.warning('网络已断开,操作将在恢复后同步')
}
}, [networkStatus])
// 更新待处理数量
useEffect(() => {
const interval = setInterval(() => {
setPendingCount(offlineQueue.getState().queuedOperations)
}, 1000)
return () => clearInterval(interval)
}, [])
// 查询 todos(离线时使用缓存)
const { data: todos } = useEnhanc