accelint-tanstack-query-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query Best Practices
TanStack Query 最佳实践
Expert patterns for TanStack Query in modern React applications with Next.js App Router and Server Components.
适用于搭配Next.js App Router和Server Components的现代React应用中的TanStack Query专家模式。
NEVER Do With TanStack Query
TanStack Query 绝对不要做的事
- NEVER use a singleton QueryClient on the server - Creates data leakage between users and race conditions. Each request must get its own isolated QueryClient instance to prevent cached data from one user appearing for another.
- NEVER synchronize query data to useState - Background refetches, invalidations, and optimistic updates all modify the cache. Local state copies become stale immediately, causing "my save didn't work" bugs. Use query data directly or derive with useMemo.
- NEVER put queries inside list item components - Creates N observers for N items, causing O(n) iteration on every cache update. 200 list items calling useQuery creates 200 network requests and 200 observers. Hoist queries to parent components.
- NEVER use unstable query keys - Arrays with non-guaranteed order, temporal queries with Date.now(), or object keys without deterministic serialization create infinite cache entries. Keys must be stable and deterministic.
- NEVER skip enabled guards for dependent queries - Firing queries with undefined parameters creates garbage cache entries like ['tracks', undefined] and wastes network requests before real data arrives.
- NEVER ignore AbortController signals - Without query cancellation support, unmounted components leave in-flight requests running, wasting bandwidth and potentially updating stale cache entries.
- NEVER use optimistic updates for high-stakes or external mutations - Life-critical operations, audit trail systems, and mutations triggered by external events need pessimistic updates to ensure UI matches server state.
- NEVER assume structural sharing is free - For datasets >1000 items updating frequently, structural sharing's O(n) deep equality checks become CPU overhead. Disable with structuralSharing: false for large, frequently-changing data.
- NEVER skip onSettled in optimistic updates - onSettled is your cleanup guarantee even if onError throws. Without it, UI can be left in corrupted state when error handler fails. Always pair onMutate with onSettled for resource cleanup and cache consistency.
- NEVER assume cache invalidation is synchronous - invalidateQueries triggers background refetches which can race with optimistic updates. Use cancelQueries in onMutate to prevent background refetches from overwriting your optimistic changes before the mutation completes.
- NEVER use setQueryData without structural comparison - Directly setting cache data bypasses structural sharing and breaks referential equality optimizations. Wrap in updater function to preserve references for unchanged portions: instead of
setQueryData(key, (old) => ({ ...old, changed: value })).setQueryData(key, newValue) - NEVER forget to handle hydration mismatches - Server-rendered data may differ from client expectations (timestamps, user-specific data, randomized content). Use suppressHydrationWarning on containers or ensure deterministic server/client rendering with stable timestamps and consistent data sources.
- 绝对不要在服务端使用单例QueryClient - 会导致用户间的数据泄露和竞态条件。每个请求都必须获取独立的QueryClient实例,防止一个用户的缓存数据出现在另一个用户的页面中。
- 绝对不要将查询数据同步到useState - 后台重新获取、缓存失效和乐观更新都会修改缓存。本地状态副本会立即过期,导致“我的保存没生效”类的bug。直接使用查询数据,或通过useMemo派生数据。
- 绝对不要在列表项组件内部放置查询 - N个列表项会创建N个观察者,每次缓存更新都会触发O(n)的迭代操作。200个列表项调用useQuery会产生200次网络请求和200个观察者。应将查询提升到父组件中。
- 绝对不要使用不稳定的查询键 - 包含顺序不固定的数组、带有Date.now()的临时查询,或未进行确定性序列化的对象键,都会创建无限多的缓存条目。查询键必须稳定且具有确定性。
- 绝对不要跳过依赖查询的enabled守卫 - 使用未定义参数触发查询会创建类似['tracks', undefined]的无效缓存条目,在真实数据到达前浪费网络请求。
- 绝对不要忽略AbortController信号 - 如果不支持查询取消,已卸载组件的未完成请求会继续运行,浪费带宽并可能更新过期的缓存条目。
- 绝对不要对高风险或外部突变使用乐观更新 - 生命关键操作、审计追踪系统,以及由外部事件触发的突变,需要使用悲观更新来确保UI与服务端状态一致。
- 绝对不要假设结构共享是无成本的 - 对于超过1000条且频繁更新的数据集,结构共享的O(n)深度相等检查会成为CPU开销。对于大型、频繁变更的数据,应设置structuralSharing: false来禁用该特性。
- 绝对不要在乐观更新中跳过onSettled - 即使onError抛出异常,onSettled也能保证资源清理。如果没有它,当错误处理程序失败时,UI可能会处于损坏状态。始终将onMutate与onSettled配对使用,以确保资源清理和缓存一致性。
- 绝对不要假设缓存失效是同步的 - invalidateQueries会触发后台重新获取,这可能与乐观更新产生竞态。在onMutate中使用cancelQueries可以防止后台重新获取在突变完成前覆盖你的乐观更改。
- 绝对不要在没有结构比较的情况下使用setQueryData - 直接设置缓存数据会绕过结构共享,破坏引用相等性优化。使用更新器函数来保留未变更部分的引用:,而不是
setQueryData(key, (old) => ({ ...old, changed: value }))。setQueryData(key, newValue) - 绝对不要忘记处理 hydration 不匹配问题 - 服务端渲染的数据可能与客户端预期不符(如时间戳、用户特定数据、随机内容)。可以在容器上使用suppressHydrationWarning,或通过稳定的时间戳和一致的数据源确保服务端/客户端渲染的确定性。
Before Using TanStack Query, Ask
使用TanStack Query前需要考虑的问题
State Classification
状态分类
- Is this server state or client state? TanStack Query manages server state (API data, database records, external system state). UI state (modals, themes, form drafts) belongs in Zustand or useState.
- Does this data change after initial render? Static reference data might not need TanStack Query's refetching machinery. Consider if simpler alternatives suffice.
- 这是服务端状态还是客户端状态? TanStack Query用于管理服务端状态(API数据、数据库记录、外部系统状态)。UI状态(模态框、主题、表单草稿)应使用Zustand或useState管理。
- 初始渲染后数据会变化吗? 静态参考数据可能不需要TanStack Query的重新获取机制。可以考虑使用更简单的替代方案。
Cache Strategy
缓存策略
- How fresh does this data need to be? Lookup tables can have 1-hour staleTime. Real-time tracking needs 5-second staleTime with refetchInterval. Match configuration to business requirements.
- What's the query lifecycle? Frequently-accessed data needs higher gcTime. One-time detail views can have aggressive garbage collection.
- 数据需要多新鲜? 查找表可以设置1小时的staleTime。实时追踪数据需要5秒的staleTime搭配refetchInterval。根据业务需求配置参数。
- 查询的生命周期是怎样的? 频繁访问的数据需要更高的gcTime。一次性详情页视图可以设置更激进的垃圾回收策略。
Observer Economics
观察者成本
- How many components will subscribe to this query? >10 observers on a single cache entry suggests hoisting queries to parent. >100 observers indicates architectural issues.
- Am I creating N queries or 1 query with N observers? List items should receive props from parent query, not call individual useQuery hooks.
- 有多少组件会订阅这个查询? 单个缓存条目有超过10个观察者时,建议将查询提升到父组件。超过100个观察者则表明存在架构问题。
- 我是在创建N个查询,还是1个查询搭配N个观察者? 列表项应从父组件接收数据作为props,而不是单独调用useQuery钩子。
How to Use
使用方法
This skill uses progressive disclosure to minimize context usage. Load references based on your scenario:
本指南采用渐进式披露方式,以最小化上下文负载。根据你的场景加载对应的参考内容:
Scenario 1: Setting Up Query Client
场景1:设置Query Client
MANDATORY - READ ENTIRE FILE: Read (~125 lines) and (~151 lines) completely for server/client setup patterns.
Do NOT Load other references for initial setup.
query-client-setup.mdserver-integration.mdCopy assets/query-client.ts for production-ready configuration.
必须完整阅读文件:阅读(约125行)和(约151行),了解服务端/客户端设置模式。
请勿加载其他参考内容用于初始设置。
query-client-setup.mdserver-integration.md复制assets/query-client.ts作为生产环境可用的配置文件。
Scenario 2: Building Query Hooks
场景2:构建查询钩子
- MANDATORY: Read (~151 lines) for key factory setup
query-keys.md - If using server components: Read
server-integration.md - Do NOT Load mutations-and-updates.md unless implementing mutations
Use decision tables below for configuration values.
- 必须阅读:阅读(约151行),了解查询键工厂的设置
query-keys.md - 如果使用Server Components:阅读
server-integration.md - 请勿加载mutations-and-updates.md,除非你要实现突变功能
使用下方的配置决策表选择参数值。
Scenario 3: Implementing Mutations
场景3:实现突变
MANDATORY - READ ENTIRE FILE: Read (~345 lines) completely. Reference for rollback patterns.
Do NOT Load caching-strategy.md for basic CRUD mutations.
mutations-and-updates.mdpatterns-and-pitfalls.md必须完整阅读文件:阅读(约345行)。参考了解回滚模式。
请勿加载caching-strategy.md用于基础CRUD突变。
mutations-and-updates.mdpatterns-and-pitfalls.mdScenario 4: Debugging Performance Issues
场景4:调试性能问题
- First, check Observer Count Thresholds table below (lines 121-129)
- If observer count >50: Read
patterns-and-pitfalls.md - If large dataset issues: Read for structural sharing
fundamentals.md - Do NOT Load all references - diagnose first, then load targeted content
- 首先,查看下方的观察者数量阈值表(第121-129行)
- 如果观察者数量>50:阅读
patterns-and-pitfalls.md - 如果是大型数据集问题:阅读了解结构共享
fundamentals.md - 请勿加载所有参考内容 - 先诊断问题,再加载针对性内容
Scenario 5: Multi-Layer Caching Strategy
场景5:多层缓存策略
MANDATORY: Read (~198 lines) for unified Next.js use cache + TanStack Query + HTTP cache patterns.
Do NOT Load if only using client-side TanStack Query.
caching-strategy.md必须阅读:阅读(约198行),了解Next.js use cache + TanStack Query + HTTP缓存的统一模式。
如果仅使用客户端TanStack Query,请勿加载该文件。
caching-strategy.mdQuery Configuration Decision Matrix
查询配置决策矩阵
| Data Type | staleTime | gcTime | refetchInterval | structuralSharing | Notes |
|---|---|---|---|---|---|
| Reference/Lookup | 1hr | Infinity | - | true | Countries, categories, static enums |
| User Profile | 5min | 10min | - | true | Changes infrequently, moderate freshness |
| Real-time Tracking | 5s | 30s | 5s | false | High update frequency, large payloads |
| Live Dashboard | 2s | 1min | 2s | Depends on size | Balance freshness vs performance |
| Detail View | 30s | 2min | - | true | Fetched on-demand, moderate caching |
| Search Results | 1min | 5min | - | true | Cacheable, not time-sensitive |
| 数据类型 | staleTime | gcTime | refetchInterval | structuralSharing | 说明 |
|---|---|---|---|---|---|
| 参考/查找数据 | 1小时 | 无限 | - | true | 国家列表、分类、静态枚举 |
| 用户资料 | 5分钟 | 10分钟 | - | true | 变更频率低,需要中等新鲜度 |
| 实时追踪数据 | 5秒 | 30秒 | 5秒 | false | 更新频率高, payload大 |
| 实时仪表盘 | 2秒 | 1分钟 | 2秒 | 取决于数据大小 | 在新鲜度和性能之间取得平衡 |
| 详情页视图 | 30秒 | 2分钟 | - | true | 按需获取,中等缓存时长 |
| 搜索结果 | 1分钟 | 5分钟 | - | true | 可缓存,对时间不敏感 |
Mutation Pattern Selection
突变模式选择
| Scenario | Pattern | When to Use |
|---|---|---|
| Form submission | Pessimistic | Multi-step forms, server validation required, error messages needed before proceeding |
| Toggle/checkbox | Optimistic | Binary state changes, low latency required, easy to rollback |
| Drag and drop | Optimistic | Immediate visual feedback essential, reordering operations, non-critical data |
| Batch operations | Pessimistic | Multiple items, partial failures possible, user needs confirmation of what succeeded |
| Life-critical ops | Pessimistic | Medical, financial, safety-critical systems where UI must match server reality |
| Audit trail required | Pessimistic | Compliance systems where operator actions must match logged events exactly |
| 场景 | 模式 | 使用时机 |
|---|---|---|
| 表单提交 | 悲观模式 | 多步骤表单、需要服务端校验、需要提前获取错误信息 |
| 切换/复选框 | 乐观模式 | 二元状态变更、低延迟需求、易于回滚 |
| 拖拽操作 | 乐观模式 | 即时视觉反馈至关重要、重排序操作、非关键数据 |
| 批量操作 | 悲观模式 | 多项目操作、可能出现部分失败、用户需要确认成功项 |
| 生命关键操作 | 悲观模式 | 医疗、金融、安全关键系统,要求UI与服务端状态完全一致 |
| 需要审计追踪 | 悲观模式 | 合规系统,要求操作员操作与日志事件完全匹配 |
Query Key Architecture
查询键架构
Use hierarchical factories for consistent invalidation:
typescript
// Recommended structure
export const keys = {
all: () => ['domain'] as const,
lists: () => [...keys.all(), 'list'] as const,
list: (filters: string) => [...keys.lists(), filters] as const,
details: () => [...keys.all(), 'detail'] as const,
detail: (id: string) => [...keys.details(), id] as const,
};
// Invalidation examples
queryClient.invalidateQueries({ queryKey: keys.all() }); // Invalidate everything
queryClient.invalidateQueries({ queryKey: keys.lists() }); // Invalidate all lists
queryClient.invalidateQueries({ queryKey: keys.detail(id) }); // Invalidate one itemKey stability rules:
- Deterministic serialization (sort arrays before joining)
- No temporal values (Date.now(), random IDs)
- Type consistency (don't mix '1' and 1)
- Stable object shapes (use sorted keys or serialize)
使用分层工厂模式实现一致的缓存失效:
typescript
// Recommended structure
export const keys = {
all: () => ['domain'] as const,
lists: () => [...keys.all(), 'list'] as const,
list: (filters: string) => [...keys.lists(), filters] as const,
details: () => [...keys.all(), 'detail'] as const,
detail: (id: string) => [...keys.details(), id] as const,
};
// Invalidation examples
queryClient.invalidateQueries({ queryKey: keys.all() }); // Invalidate everything
queryClient.invalidateQueries({ queryKey: keys.lists() }); // Invalidate all lists
queryClient.invalidateQueries({ queryKey: keys.detail(id) }); // Invalidate one item查询键稳定性规则:
- 确定性序列化(拼接前先对数组排序)
- 不包含临时值(Date.now()、随机ID)
- 类型一致性(不要混合使用'1'和1)
- 稳定的对象结构(使用排序后的键或进行序列化)
Server-Client Integration Pattern
服务端-客户端集成模式
| Layer | Purpose | Invalidation Method | Cache Scope |
|---|---|---|---|
| Next.js use cache | Reduce database load | revalidateTag() or updateTag() | Cross-request, server-side |
| TanStack Query | Client-side state management | queryClient.invalidateQueries() | Per-browser-tab |
| Browser HTTP cache | Eliminate network requests | Cache-Control headers | Per-browser |
Unified invalidation strategy:
- Use same key factories for both server and client caches
- Server mutations call updateTag(...keys.detail(id))
- Client mutations call queryClient.invalidateQueries({ queryKey: keys.detail(id) })
- Both caches stay synchronized with same hierarchy
| 层级 | 用途 | 失效方式 | 缓存范围 |
|---|---|---|---|
| Next.js use cache | 减少数据库负载 | revalidateTag() 或 updateTag() | 跨请求、服务端 |
| TanStack Query | 客户端状态管理 | queryClient.invalidateQueries() | 每个浏览器标签页 |
| 浏览器HTTP缓存 | 消除网络请求 | Cache-Control 响应头 | 每个浏览器 |
统一失效策略:
- 服务端和客户端缓存使用相同的键工厂
- 服务端突变调用updateTag(...keys.detail(id))
- 客户端突变调用queryClient.invalidateQueries({ queryKey: keys.detail(id) })
- 两个缓存通过相同的层级结构保持同步
Observer Count Thresholds
观察者数量阈值
| Observer Count | Performance Impact | Action Required |
|---|---|---|
| 1-5 | Negligible | None |
| 6-20 | Minimal | Monitor, no immediate action |
| 21-50 | Noticeable on updates | Consider hoisting queries to parent |
| 51-100 | Significant overhead | Refactor: hoist queries or use select |
| 100+ | Critical impact | Immediate refactor: single query with props distribution |
Diagnosis:
- Open TanStack Query DevTools in development
- Find cache entries with high observer counts
- Search codebase for useQuery calls with those keys
- Refactor to parent components or shared cache entries
| 观察者数量 | 性能影响 | 所需操作 |
|---|---|---|
| 1-5 | 可忽略 | 无 |
| 6-20 | 极小 | 监控,无需立即操作 |
| 21-50 | 更新时可察觉 | 考虑将查询提升到父组件 |
| 51-100 | 显著开销 | 重构:提升查询或使用select |
| 100+ | 严重影响 | 立即重构:使用单个查询并通过props分发数据 |
诊断步骤:
- 在开发环境中打开TanStack Query DevTools
- 找到观察者数量较多的缓存条目
- 在代码库中搜索使用这些键的useQuery调用
- 重构为父组件查询或共享缓存条目
Query Hook Patterns
查询钩子模式
| Pattern | Use Case | Example |
|---|---|---|
| useSuspenseQuery | Server Components integration, Suspense boundaries | |
| useQuery with enabled | Dependent queries, conditional fetching | |
| useQuery with select | Data transformation, subset selection | |
| useMutation optimistic | Low-latency UI updates, easily reversible | |
| useMutation pessimistic | High-stakes operations, server validation | |
| 模式 | 使用场景 | 示例 |
|---|---|---|
| useSuspenseQuery | Server Components集成、Suspense边界 | |
| 带enabled的useQuery | 依赖查询、条件获取 | |
| 带select的useQuery | 数据转换、子集选择 | |
| 乐观更新的useMutation | 低延迟UI更新、易于回滚 | |
| 悲观更新的useMutation | 高风险操作、服务端校验 | |
Common Error Patterns and Fixes
常见错误模式及修复方案
| Symptom | Root Cause | Solution | Fallback if Solution Fails |
|---|---|---|---|
| Data doesn't update after save | Copied query data to useState | Use query data directly, derive with useMemo | Force refetch with refetch() method, check network tab for actual API response |
| Infinite requests | Unstable query keys (Date.now(), unsorted arrays) | Use deterministic key construction | Add staleness detection: |
| N duplicate requests | Query in every list item | Hoist query to parent, pass data as props | Ensure all components use identical queryKey (same object reference or values): |
| Query fires with undefined params | Missing enabled guard | Add | Use placeholderData to show loading state, add type guards in queryFn to throw early |
| Slow list rendering | N queries + N observers | Single parent query, distribute via props | Use select to subscribe to subset, implement virtual scrolling to reduce mounted components |
| Cache never clears | gcTime: Infinity on frequently-changing data | Match gcTime to data lifecycle | Force removal with queryClient.removeQueries(), monitor cache size with DevTools |
| UI shows stale data flash | Server cache stale, client cache fresh | Unified invalidation with same keys | Use initialData from server props, set refetchOnMount: false for hydrated queries |
| Optimistic update won't rollback | onError not restoring context | Use context from onMutate in onError | Force invalidation with invalidateQueries, implement manual rollback with previous state snapshot |
| Server hydration mismatch | Timestamp/user-specific data in SSR | Use suppressHydrationWarning on container | Client-only rendering with dynamic import and ssr: false, or normalize timestamps to UTC |
| Query never refetches | enabled: false guard blocking, or gcTime expired | Check enabled conditions, verify query isn't filtered by predicate | Increase gcTime to keep cache alive longer, use refetchInterval for polling behavior, check if staleTime: Infinity is preventing background refetches |
| Server action not invalidating | updateTag/revalidateTag using different keys than queryClient | Use same key factories for both server and client caches | Manually call router.refresh() after server action, verify tag names match query key hierarchy |
| Mutation succeeds but UI doesn't update | Missing onSuccess invalidation or wrong queryKey | Add | Use setQueryData to manually update cache: |
| 症状 | 根本原因 | 解决方案 | 方案失败时的 fallback |
|---|---|---|---|
| 保存后数据未更新 | 将查询数据复制到了useState | 直接使用查询数据,或通过useMemo派生 | 使用refetch()方法强制重新获取,检查网络面板确认实际API响应 |
| 无限请求 | 不稳定的查询键(Date.now()、未排序数组) | 使用确定性的查询键构建方式 | 添加过期检测: |
| N次重复请求 | 每个列表项中都有查询 | 将查询提升到父组件,通过props传递数据 | 确保所有组件使用完全相同的queryKey(相同的对象引用或值): |
| 查询使用未定义参数触发 | 缺失enabled守卫 | 添加 | 使用placeholderData展示加载状态,在queryFn中添加类型守卫提前抛出异常 |
| 列表渲染缓慢 | N个查询 + N个观察者 | 单个父组件查询,通过props分发数据 | 使用select订阅子集数据,实现虚拟滚动减少挂载的组件数量 |
| 缓存永不清除 | 频繁变更的数据设置了gcTime: Infinity | 将gcTime与数据生命周期匹配 | 使用queryClient.removeQueries()强制移除,通过DevTools监控缓存大小 |
| UI显示过期数据闪烁 | 服务端缓存过期,客户端缓存新鲜 | 使用相同的键实现统一失效 | 从服务端props获取initialData,为已hydrate的查询设置refetchOnMount: false |
| 乐观更新无法回滚 | onError未恢复上下文 | 在onError中使用onMutate返回的上下文 | 使用invalidateQueries强制失效,使用之前的状态快照实现手动回滚 |
| 服务端Hydration不匹配 | SSR中包含时间戳/用户特定数据 | 在容器上使用suppressHydrationWarning | 使用动态导入和ssr: false实现仅客户端渲染,或将时间戳标准化为UTC |
| 查询从不重新获取 | enabled: false守卫阻止,或gcTime已过期 | 检查enabled条件,验证查询未被谓词过滤 | 增加gcTime以延长缓存存活时间,使用refetchInterval实现轮询,检查是否staleTime: Infinity阻止了后台重新获取 |
| 服务端操作未触发失效 | updateTag/revalidateTag使用的键与queryClient不同 | 服务端和客户端缓存使用相同的键工厂 | 服务端操作后手动调用router.refresh(),验证标签名称与查询键层级匹配 |
| 突变成功但UI未更新 | 缺失onSuccess失效或查询键错误 | 添加 | 使用setQueryData手动更新缓存: |
Troubleshooting Decision Tree
故障排查决策树
Performance Issues
性能问题
Step 1: Check observer count in DevTools (use thresholds at lines 136-145)
- >100 observers → Immediate refactor: hoist queries to parent component, distribute data via props
- 51-100 observers → Refactor: hoist queries or use select to subscribe to data subsets
- <50 observers → Issue is elsewhere, continue to Step 2
Step 2: Check data size and update frequency
- >1000 items + frequent updates → Disable structural sharing: (see fundamentals.md for details)
structuralSharing: false - Large payloads (>500KB) → Check network tab, consider pagination or infinite queries
- Fast updates (<1s interval) → Lower staleTime or use refetchInterval, verify cache strategy
Step 3: Check React DevTools Profiler
- Look for unnecessary re-renders in components using query data
- Verify select function isn't recreated on every render (use useCallback)
- Check if derived data should use useMemo instead of inline transformation
- Profile component render times to identify bottlenecks
步骤1:在DevTools中检查观察者数量(使用第136-145行的阈值)
- >100个观察者 → 立即重构:将查询提升到父组件,通过props分发数据
- 51-100个观察者 → 重构:提升查询或使用select订阅数据子集
- <50个观察者 → 问题出在其他地方,继续步骤2
步骤2:检查数据大小和更新频率
- >1000条数据 + 频繁更新 → 禁用结构共享:(详情见fundamentals.md)
structuralSharing: false - 大Payload(>500KB) → 检查网络面板,考虑分页或无限查询
- 快速更新(<1秒间隔) → 降低staleTime或使用refetchInterval,验证缓存策略
步骤3:检查React DevTools Profiler
- 查找使用查询数据的组件中不必要的重新渲染
- 验证select函数不会在每次渲染时重新创建(使用useCallback)
- 检查派生数据是否应使用useMemo而非内联转换
- 分析组件渲染时间以确定瓶颈
Network Issues
网络问题
Flaky connections:
- Configure retry logic:
retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) - See query-client-setup.md for production retry configuration
Token refresh needed:
- Implement auth interceptor pattern in queryFn wrapper
- Use queryClient.setQueryDefaults() for global auth headers
- See patterns-and-pitfalls.md for token refresh patterns
Race conditions:
- Review invalidation timing: use cancelQueries before setQueryData
- Check if optimistic updates compete with background refetches
- Verify mutation onMutate uses await cancelQueries({ queryKey })
不稳定连接:
- 配置重试逻辑:
retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) - 查看query-client-setup.md了解生产环境重试配置
需要刷新Token:
- 在queryFn包装器中实现认证拦截器模式
- 使用queryClient.setQueryDefaults()设置全局认证头
- 查看patterns-and-pitfalls.md了解Token刷新模式
竞态条件:
- 检查失效时机:在setQueryData前使用cancelQueries
- 检查乐观更新是否与后台重新获取产生竞态
- 验证突变的onMutate是否使用了await cancelQueries({ queryKey })
Hydration Issues
Hydration问题
SSR mismatch (hydration error in console):
- Add suppressHydrationWarning to container element
- Normalize data: ensure server and client produce identical output (stable timestamps, sorted arrays)
- Check if user-specific data is being rendered server-side
Client-server data drift:
- Verify revalidateTag timing on server mutations
- Check if server cache (Next.js use cache) is stale while client cache is fresh
- Use initialData from server props:
initialData: serverData, refetchOnMount: false
HydrationBoundary not working:
- Verify client component boundaries: HydrationBoundary must wrap 'use client' components
- Check if dehydratedState is being serialized correctly from server
- Ensure shouldDehydrateQuery includes queries you want to hydrate
SSR不匹配(控制台出现hydration错误):
- 在容器元素上添加suppressHydrationWarning
- 标准化数据:确保服务端和客户端生成完全相同的输出(稳定的时间戳、排序后的数组)
- 检查是否在服务端渲染了用户特定数据
客户端-服务端数据漂移:
- 验证服务端突变的revalidateTag时机
- 检查服务端缓存(Next.js use cache)是否过期,而客户端缓存仍为新鲜
- 使用服务端props作为initialData:
initialData: serverData, refetchOnMount: false
HydrationBoundary不生效:
- 验证客户端组件边界:HydrationBoundary必须包裹'use client'组件
- 检查dehydratedState是否从服务端正确序列化
- 确保shouldDehydrateQuery包含你想要hydrate的查询
Freedom Calibration
自由度校准
Calibrate guidance specificity to mutation risk:
| Task Type | Freedom Level | Guidance Format | Example |
|---|---|---|---|
| Query configuration | High freedom | Principles with tables for common patterns | "Match staleTime to business requirements" |
| Optimistic updates | Medium freedom | Complete pattern with rollback handling | "Use onMutate/onError/onSettled callbacks" |
| QueryClient setup | Low freedom | Exact code with critical security warning | "NEVER use singleton on server - use factory" |
The test: "If the agent makes a mistake, what's the consequence?"
- Server singleton mistake → Data leakage between users (critical security issue)
- Observer count mistake → Performance degradation (medium impact)
- staleTime tuning → Suboptimal freshness (low impact)
根据突变风险调整指导的具体程度:
| 任务类型 | 自由度 | 指导格式 | 示例 |
|---|---|---|---|
| 查询配置 | 高自由度 | 原则 + 常见模式表格 | "根据业务需求匹配staleTime" |
| 乐观更新 | 中等自由度 | 完整模式 + 回滚处理 | "使用onMutate/onError/onSettled回调" |
| QueryClient设置 | 低自由度 | 精确代码 + 关键安全警告 | "绝对不要在服务端使用单例 - 使用工厂模式" |
测试标准: "如果助手出错,后果是什么?"
- 服务端单例错误 → 用户间数据泄露(严重安全问题)
- 观察者数量错误 → 性能下降(中等影响)
- staleTime调优错误 → 新鲜度不理想(低影响)
Important Notes
重要提示
- Query keys are hashed deterministically - ['tracks', '1'] and ['tracks', 1] create different cache entries
- Query keys must be JSON-serializable for cache persistence across page reloads and hydration
- shouldDehydrateQuery with pending status enables streaming without await in server components
- HydrationBoundary must wrap client components only - server components bypass the boundary
- revalidateTag vs updateTag matters: revalidateTag uses stale-while-revalidate, updateTag invalidates immediately
- Background refetches run even when no components are mounted if gcTime hasn't expired
- Structural sharing runs twice when using select: once on raw data, once on transformed data
- cancelQueries in onMutate is critical - background refetches can overwrite optimistic updates
- Context returned from onMutate is passed to onError and onSettled for rollback state
- 查询键会被确定性哈希处理 - ['tracks', '1']和['tracks', 1]会创建不同的缓存条目
- 查询键必须是可JSON序列化的,以支持跨页面刷新和hydration的缓存持久化
- 对pending状态的查询使用shouldDehydrateQuery,可实现Server Components中的流式渲染无需await
- HydrationBoundary必须仅包裹客户端组件 - Server Components会绕过该边界
- revalidateTag和updateTag的区别:revalidateTag使用stale-while-revalidate模式,updateTag会立即失效
- 如果gcTime未过期,即使没有组件挂载,后台重新获取也会运行
- 使用select时,结构共享会运行两次:一次在原始数据上,一次在转换后的数据上
- 在onMutate中使用cancelQueries至关重要 - 后台重新获取可能会覆盖乐观更新
- onMutate返回的上下文会传递给onError和onSettled,用于回滚状态