accelint-tanstack-query-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack 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:
    setQueryData(key, (old) => ({ ...old, changed: value }))
    instead of
    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
query-client-setup.md
(~125 lines) and
server-integration.md
(~151 lines) completely for server/client setup patterns. Do NOT Load other references for initial setup.
Copy assets/query-client.ts for production-ready configuration.
必须完整阅读文件:阅读
query-client-setup.md
(约125行)和
server-integration.md
(约151行),了解服务端/客户端设置模式。 请勿加载其他参考内容用于初始设置。
复制assets/query-client.ts作为生产环境可用的配置文件。

Scenario 2: Building Query Hooks

场景2:构建查询钩子

  1. MANDATORY: Read
    query-keys.md
    (~151 lines) for key factory setup
  2. If using server components: Read
    server-integration.md
  3. Do NOT Load mutations-and-updates.md unless implementing mutations
Use decision tables below for configuration values.
  1. 必须阅读:阅读
    query-keys.md
    (约151行),了解查询键工厂的设置
  2. 如果使用Server Components:阅读
    server-integration.md
  3. 请勿加载mutations-and-updates.md,除非你要实现突变功能
使用下方的配置决策表选择参数值。

Scenario 3: Implementing Mutations

场景3:实现突变

MANDATORY - READ ENTIRE FILE: Read
mutations-and-updates.md
(~345 lines) completely. Reference
patterns-and-pitfalls.md
for rollback patterns. Do NOT Load caching-strategy.md for basic CRUD mutations.
必须完整阅读文件:阅读
mutations-and-updates.md
(约345行)。参考
patterns-and-pitfalls.md
了解回滚模式。 请勿加载caching-strategy.md用于基础CRUD突变。

Scenario 4: Debugging Performance Issues

场景4:调试性能问题

  1. First, check Observer Count Thresholds table below (lines 121-129)
  2. If observer count >50: Read
    patterns-and-pitfalls.md
  3. If large dataset issues: Read
    fundamentals.md
    for structural sharing
  4. Do NOT Load all references - diagnose first, then load targeted content
  1. 首先,查看下方的观察者数量阈值表(第121-129行)
  2. 如果观察者数量>50:阅读
    patterns-and-pitfalls.md
  3. 如果是大型数据集问题:阅读
    fundamentals.md
    了解结构共享
  4. 请勿加载所有参考内容 - 先诊断问题,再加载针对性内容

Scenario 5: Multi-Layer Caching Strategy

场景5:多层缓存策略

MANDATORY: Read
caching-strategy.md
(~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,请勿加载该文件。

Query Configuration Decision Matrix

查询配置决策矩阵

Data TypestaleTimegcTimerefetchIntervalstructuralSharingNotes
Reference/Lookup1hrInfinity-trueCountries, categories, static enums
User Profile5min10min-trueChanges infrequently, moderate freshness
Real-time Tracking5s30s5sfalseHigh update frequency, large payloads
Live Dashboard2s1min2sDepends on sizeBalance freshness vs performance
Detail View30s2min-trueFetched on-demand, moderate caching
Search Results1min5min-trueCacheable, not time-sensitive
数据类型staleTimegcTimerefetchIntervalstructuralSharing说明
参考/查找数据1小时无限-true国家列表、分类、静态枚举
用户资料5分钟10分钟-true变更频率低,需要中等新鲜度
实时追踪数据5秒30秒5秒false更新频率高, payload大
实时仪表盘2秒1分钟2秒取决于数据大小在新鲜度和性能之间取得平衡
详情页视图30秒2分钟-true按需获取,中等缓存时长
搜索结果1分钟5分钟-true可缓存,对时间不敏感

Mutation Pattern Selection

突变模式选择

ScenarioPatternWhen to Use
Form submissionPessimisticMulti-step forms, server validation required, error messages needed before proceeding
Toggle/checkboxOptimisticBinary state changes, low latency required, easy to rollback
Drag and dropOptimisticImmediate visual feedback essential, reordering operations, non-critical data
Batch operationsPessimisticMultiple items, partial failures possible, user needs confirmation of what succeeded
Life-critical opsPessimisticMedical, financial, safety-critical systems where UI must match server reality
Audit trail requiredPessimisticCompliance 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 item
Key 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

服务端-客户端集成模式

LayerPurposeInvalidation MethodCache Scope
Next.js use cacheReduce database loadrevalidateTag() or updateTag()Cross-request, server-side
TanStack QueryClient-side state managementqueryClient.invalidateQueries()Per-browser-tab
Browser HTTP cacheEliminate network requestsCache-Control headersPer-browser
Unified invalidation strategy:
  1. Use same key factories for both server and client caches
  2. Server mutations call updateTag(...keys.detail(id))
  3. Client mutations call queryClient.invalidateQueries({ queryKey: keys.detail(id) })
  4. Both caches stay synchronized with same hierarchy
层级用途失效方式缓存范围
Next.js use cache减少数据库负载revalidateTag() 或 updateTag()跨请求、服务端
TanStack Query客户端状态管理queryClient.invalidateQueries()每个浏览器标签页
浏览器HTTP缓存消除网络请求Cache-Control 响应头每个浏览器
统一失效策略:
  1. 服务端和客户端缓存使用相同的键工厂
  2. 服务端突变调用updateTag(...keys.detail(id))
  3. 客户端突变调用queryClient.invalidateQueries({ queryKey: keys.detail(id) })
  4. 两个缓存通过相同的层级结构保持同步

Observer Count Thresholds

观察者数量阈值

Observer CountPerformance ImpactAction Required
1-5NegligibleNone
6-20MinimalMonitor, no immediate action
21-50Noticeable on updatesConsider hoisting queries to parent
51-100Significant overheadRefactor: hoist queries or use select
100+Critical impactImmediate refactor: single query with props distribution
Diagnosis:
  1. Open TanStack Query DevTools in development
  2. Find cache entries with high observer counts
  3. Search codebase for useQuery calls with those keys
  4. Refactor to parent components or shared cache entries
观察者数量性能影响所需操作
1-5可忽略
6-20极小监控,无需立即操作
21-50更新时可察觉考虑将查询提升到父组件
51-100显著开销重构:提升查询或使用select
100+严重影响立即重构:使用单个查询并通过props分发数据
诊断步骤:
  1. 在开发环境中打开TanStack Query DevTools
  2. 找到观察者数量较多的缓存条目
  3. 在代码库中搜索使用这些键的useQuery调用
  4. 重构为父组件查询或共享缓存条目

Query Hook Patterns

查询钩子模式

PatternUse CaseExample
useSuspenseQueryServer Components integration, Suspense boundaries
useSuspenseQuery({ queryKey, queryFn })
useQuery with enabledDependent queries, conditional fetching
useQuery({ queryKey, queryFn, enabled: !!userId })
useQuery with selectData transformation, subset selection
useQuery({ queryKey, queryFn, select: (data) => data.slice(0, 10) })
useMutation optimisticLow-latency UI updates, easily reversible
useMutation({ onMutate, onError, onSettled })
useMutation pessimisticHigh-stakes operations, server validation
useMutation({ onSuccess })
模式使用场景示例
useSuspenseQueryServer Components集成、Suspense边界
useSuspenseQuery({ queryKey, queryFn })
带enabled的useQuery依赖查询、条件获取
useQuery({ queryKey, queryFn, enabled: !!userId })
带select的useQuery数据转换、子集选择
useQuery({ queryKey, queryFn, select: (data) => data.slice(0, 10) })
乐观更新的useMutation低延迟UI更新、易于回滚
useMutation({ onMutate, onError, onSettled })
悲观更新的useMutation高风险操作、服务端校验
useMutation({ onSuccess })

Common Error Patterns and Fixes

常见错误模式及修复方案

SymptomRoot CauseSolutionFallback if Solution Fails
Data doesn't update after saveCopied query data to useStateUse query data directly, derive with useMemoForce refetch with refetch() method, check network tab for actual API response
Infinite requestsUnstable query keys (Date.now(), unsorted arrays)Use deterministic key constructionAdd staleness detection:
const requestCount = useRef(0); useEffect(() => { requestCount.current++; if (requestCount.current > 10) console.error('Infinite loop detected', queryKey); }, [data]);
See fundamentals.md for key stability patterns
N duplicate requestsQuery in every list itemHoist query to parent, pass data as propsEnsure all components use identical queryKey (same object reference or values):
const queryKey = useMemo(() => keys.list(filters), [filters]);
Increase staleTime to 30s to deduplicate rapid requests
Query fires with undefined paramsMissing enabled guardAdd
enabled: Boolean(dependency)
Use placeholderData to show loading state, add type guards in queryFn to throw early
Slow list renderingN queries + N observersSingle parent query, distribute via propsUse select to subscribe to subset, implement virtual scrolling to reduce mounted components
Cache never clearsgcTime: Infinity on frequently-changing dataMatch gcTime to data lifecycleForce removal with queryClient.removeQueries(), monitor cache size with DevTools
UI shows stale data flashServer cache stale, client cache freshUnified invalidation with same keysUse initialData from server props, set refetchOnMount: false for hydrated queries
Optimistic update won't rollbackonError not restoring contextUse context from onMutate in onErrorForce invalidation with invalidateQueries, implement manual rollback with previous state snapshot
Server hydration mismatchTimestamp/user-specific data in SSRUse suppressHydrationWarning on containerClient-only rendering with dynamic import and ssr: false, or normalize timestamps to UTC
Query never refetchesenabled: false guard blocking, or gcTime expiredCheck enabled conditions, verify query isn't filtered by predicateIncrease gcTime to keep cache alive longer, use refetchInterval for polling behavior, check if staleTime: Infinity is preventing background refetches
Server action not invalidatingupdateTag/revalidateTag using different keys than queryClientUse same key factories for both server and client cachesManually call router.refresh() after server action, verify tag names match query key hierarchy
Mutation succeeds but UI doesn't updateMissing onSuccess invalidation or wrong queryKeyAdd
onSuccess: () => queryClient.invalidateQueries({ queryKey })
Use setQueryData to manually update cache:
queryClient.setQueryData(keys.detail(id), newData)
, verify queryKey matches exactly
症状根本原因解决方案方案失败时的 fallback
保存后数据未更新将查询数据复制到了useState直接使用查询数据,或通过useMemo派生使用refetch()方法强制重新获取,检查网络面板确认实际API响应
无限请求不稳定的查询键(Date.now()、未排序数组)使用确定性的查询键构建方式添加过期检测:
const requestCount = useRef(0); useEffect(() => { requestCount.current++; if (requestCount.current > 10) console.error('Infinite loop detected', queryKey); }, [data]);
查看fundamentals.md了解查询键稳定性模式
N次重复请求每个列表项中都有查询将查询提升到父组件,通过props传递数据确保所有组件使用完全相同的queryKey(相同的对象引用或值):
const queryKey = useMemo(() => keys.list(filters), [filters]);
将staleTime增加到30秒以去重快速重复请求
查询使用未定义参数触发缺失enabled守卫添加
enabled: Boolean(dependency)
使用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失效或查询键错误添加
onSuccess: () => queryClient.invalidateQueries({ queryKey })
使用setQueryData手动更新缓存:
queryClient.setQueryData(keys.detail(id), newData)
,验证queryKey完全匹配

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:
    structuralSharing: false
    (see fundamentals.md for details)
  • 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条数据 + 频繁更新 → 禁用结构共享:
    structuralSharing: false
    (详情见fundamentals.md)
  • 大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 TypeFreedom LevelGuidance FormatExample
Query configurationHigh freedomPrinciples with tables for common patterns"Match staleTime to business requirements"
Optimistic updatesMedium freedomComplete pattern with rollback handling"Use onMutate/onError/onSettled callbacks"
QueryClient setupLow freedomExact 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,用于回滚状态