tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query v5 - Expert Troubleshooting
TanStack Query v5 - 专家级故障排查
Assumption: You know basics. This covers what breaks in production.
useQuery假设:你了解的基础知识。本文涵盖生产环境中会出现的问题。
useQueryBefore Using React Query: Strategic Assessment
使用React Query之前:策略评估
Ask yourself these questions BEFORE adding useQuery:
在添加useQuery之前,请先问自己这些问题:
1. Data Source Analysis
1. 数据源分析
- Where does this data come from?
- URL params/path → Framework loader (Next.js, Remix), not React Query
- Computation/derivation → useMemo, not React Query
- Form input → React Hook Form, not React Query
- REST/GraphQL → React Query ✅
- 数据来自哪里?
- URL 参数/路径 → 使用框架加载器(Next.js、Remix),而非React Query
- 计算/派生数据 → 使用useMemo,而非React Query
- 表单输入 → 使用React Hook Form,而非React Query
- REST/GraphQL → 使用React Query ✅
2. Update Frequency & Caching Strategy
2. 更新频率与缓存策略
- How often does this data change?
- Real-time (>1/sec) → WebSocket + Zustand (React Query overhead too high)
- Frequent (<1/min) → React Query with aggressive staleTime (30s-1min)
- Moderate (5-30min) → React Query standard (staleTime: 5min)
- Infrequent (>1hr) → React Query with long staleTime (30min+)
- 数据更新频率如何?
- 实时(>1次/秒)→ WebSocket + Zustand(React Query开销过高)
- 频繁(<1次/分钟)→ React Query搭配激进的staleTime(30秒-1分钟)
- 中等(5-30分钟)→ React Query标准配置(staleTime: 5分钟)
- 低频(>1小时)→ React Query搭配长staleTime(30分钟+)
3. Cost of Stale Data
3. 过期数据的影响
- What happens if user sees old data?
- Critical (money, auth tokens) → staleTime: 0 (always fresh)
- Important (user content, messages) → staleTime: 1-5min
- Nice-to-have (analytics, recommendations) → staleTime: 30min+
- 如果用户看到旧数据会有什么后果?
- 严重影响(资金、认证令牌)→ staleTime: 0(始终获取最新数据)
- 重要影响(用户内容、消息)→ staleTime: 1-5分钟
- 锦上添花(分析数据、推荐内容)→ staleTime: 30分钟+
Critical Decision: When NOT to Use React Query
关键决策:何时不使用React Query
Need data fetching?
│
├─ Data from URL (search params, path) → DON'T use queries
│ └─ Use framework loaders (Next.js, Remix) or URL state
│ WHY: Queries cache by key, URL is already your cache key
│
├─ Derived/computed data → DON'T use queries
│ └─ Use useMemo or Zustand
│ WHY: No server, no stale data, no refetch needed
│
├─ Form state → DON'T use queries
│ └─ Use React Hook Form or controlled state
│ WHY: Forms are local state, not server cache
│
├─ WebSocket/realtime data → MAYBE use queries
│ ├─ High-frequency updates (> 1/sec) → DON'T use queries (use Zustand)
│ └─ Low-frequency (<1/min) → Use queries with manual updates
│ WHY: Queries designed for request/response, not streaming
│
└─ REST/GraphQL server state → USE queries
(This is what React Query is for)The trap: Developers use React Query for everything. It's a server cache, not a state manager.
需要数据获取?
│
├─ 数据来自URL(搜索参数、路径)→ 不要使用查询
│ └─ 使用框架加载器(Next.js、Remix)或URL状态
│ 原因:查询按键缓存,URL本身就是你的缓存键
│
├─ 派生/计算数据 → 不要使用查询
│ └─ 使用useMemo或Zustand
│ 原因:无需服务器,无过期数据,无需重取
│
├─ 表单状态 → 不要使用查询
│ └─ 使用React Hook Form或受控状态
│ 原因:表单是本地状态,而非服务器缓存
│
├─ WebSocket/实时数据 → 可考虑使用查询
│ ├─ 高频更新(>1次/秒)→ 不要使用查询(使用Zustand)
│ └─ 低频更新(<1次/分钟)→ 使用查询搭配手动更新
│ 原因:查询为请求/响应模式设计,而非流式传输
│
└─ REST/GraphQL服务器状态 → 使用查询
(这才是React Query的适用场景)误区:开发者用React Query处理所有事情。它是服务器缓存,不是状态管理器。
Breaking Changes: v4 → v5 Migration Gotchas
破坏性变更:v4 → v5 迁移陷阱
❌ #1: cacheTime
Renamed to gcTime
cacheTimegcTime❌ #1:cacheTime
重命名为 gcTime
cacheTimegcTimeProblem: Silent failure - code runs but cache doesn't work as expected
typescript
// WRONG - v4 syntax, silently ignored in v5
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 10 * 60 * 1000, // ❌ Ignored in v5
})
// CORRECT - v5 syntax
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 10 * 60 * 1000, // ✅ Garbage collection time
})Why it breaks: TypeScript won't error if using or loose types. Cache appears to work but garbage collects immediately.
anyWhy this is deceptively hard to debug: No error messages—app runs perfectly. Cache appears functional initially. Only after 5+ minutes in production do you notice data refetching too frequently and Network tab lighting up. DevTools shows query has 0ms gcTime but you SET 10 minutes. The property is silently ignored—no warnings, no TypeScript errors. Takes 20-30 minutes of cache inspection comparing v4 docs to v5 docs to realize the property was renamed. Searches for "cacheTime not working" find v4 results, not v5 migration notes.
问题:静默失败 - 代码运行但缓存未按预期工作
typescript
// 错误 - v4语法,在v5中被静默忽略
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 10 * 60 * 1000, // ❌ v5中被忽略
})
// 正确 - v5语法
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 10 * 60 * 1000, // ✅ 垃圾回收时间
})失效原因:如果使用或松散类型,TypeScript不会报错。缓存看似正常工作,但会立即被垃圾回收。
any调试难点:无错误提示——应用运行正常。缓存初始看似可用。只有在生产环境运行5分钟以上后,才会发现数据重取过于频繁,网络面板持续活跃。DevTools显示查询的gcTime为0ms,但你明明设置了10分钟。该属性被静默忽略——无警告、无TypeScript错误。需要花费20-30分钟对比v4和v5文档检查缓存,才能发现属性被重命名。搜索“cacheTime not working”会找到v4的结果,而非v5迁移说明。
❌ #2: isLoading
Removed, Use isPending
isLoadingisPending❌ #2:isLoading
被移除,使用 isPending
isLoadingisPendingProblem: Loading spinners disappear too early
typescript
// WRONG - v4 syntax
const { isLoading } = useQuery(...)
if (isLoading) return <Spinner /> // ❌ isLoading undefined in v5
// CORRECT - v5 syntax
const { isPending } = useQuery(...)
if (isPending) return <Spinner /> // ✅ Shows while query pendingWhy different:
- (v4):
isLoadingonly for first fetch (no cached data)true - (v5):
isPendingfor first fetch + refetches (more accurate)true
Migration trap: If you have cached data and refetch, stays but was . UI shows stale data + spinner in v5.
isPendingtrueisLoadingfalseWhy this is deceptively hard to debug: is undefined but JavaScript doesn't error— is falsy, so spinner never shows. UI appears to work in initial testing (data loads fine). Only in specific edge case—user navigates away and back with cached data—does the bug appear: no loading state during refetch. Users report "page feels broken" but can't reproduce consistently. DevTools shows but that's easy to miss in large state object. Takes 15-20 minutes to realize v5 removed the property entirely and you need instead.
isLoadingif (undefined)isLoading: undefinedisPending问题:加载 spinner 过早消失
typescript
// 错误 - v4语法
const { isLoading } = useQuery(...)
if (isLoading) return <Spinner /> // ❌ v5中isLoading未定义
// 正确 - v5语法
const { isPending } = useQuery(...)
if (isPending) return <Spinner /> // ✅ 查询挂起时显示差异原因:
- (v4):仅在首次获取(无缓存数据)时为
isLoadingtrue - (v5):在首次获取和重取时均为
isPending(更准确)true
迁移误区:如果有缓存数据并重取,保持,但旧的为。v5中UI会显示过期数据+spinner。
isPendingtrueisLoadingfalse调试难点:未定义,但JavaScript不会报错——为假,因此spinner永远不显示。初始测试中UI看似正常(数据加载正常)。仅在特定边缘场景——用户导航离开并返回(带有缓存数据)——才会出现bug:重取期间无加载状态。用户反馈“页面似乎崩溃”但无法稳定复现。DevTools显示,但在庞大的状态对象中容易被忽略。需要15-20分钟才能意识到v5完全移除了该属性,需改用。
isLoadingif (undefined)isLoading: undefinedisPending❌ #3: keepPreviousData
Removed, Use placeholderData
keepPreviousDataplaceholderData❌ #3:keepPreviousData
被移除,使用 placeholderData
keepPreviousDataplaceholderDataProblem: Pagination breaks - flickers on page change
typescript
// WRONG - v4 syntax
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true, // ❌ Removed in v5
})
// CORRECT - v5 syntax
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData, // ✅ Function form
})Why it breaks: was boolean. is data OR function. If you pass , TypeScript error but runtime breaks.
keepPreviousData: trueplaceholderDatatrue问题:分页失效 - 切换页面时出现闪烁
typescript
// 错误 - v4语法
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true, // ❌ v5中被移除
})
// 正确 - v5语法
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData, // ✅ 函数形式
})失效原因:是布尔值。是数据或函数。如果传入,TypeScript会报错,运行时也会失效。
keepPreviousData: trueplaceholderDatatrue❌ #4: Query Functions Must Return Non-Void
❌ #4:查询函数必须返回非空值
Problem: Mutations that don't return data break
typescript
// WRONG - void return breaks v5
queryFn: async () => {
await api.deleteTodo(id) // ❌ Returns void
}
// CORRECT - return something
queryFn: async () => {
await api.deleteTodo(id)
return { success: true } // ✅ Return data
}Why it breaks: v5 type system requires , not . Silent runtime error if using .
Promise<TData>Promise<void>any问题:不返回数据的变更会失效
typescript
// 错误 - void返回会导致v5失效
queryFn: async () => {
await api.deleteTodo(id) // ❌ 返回void
}
// 正确 - 返回内容
queryFn: async () => {
await api.deleteTodo(id)
return { success: true } // ✅ 返回数据
}失效原因:v5类型系统要求,而非。如果使用会出现静默运行时错误。
Promise<TData>Promise<void>anyPerformance Pitfalls
性能隐患
❌ Infinite Refetch Loop
❌ 无限重取循环
Problem: Query refetches forever, browser freezes
typescript
// WRONG - creates infinite loop
useQuery({
queryKey: ['user', user], // ❌ Object in key
queryFn: () => fetchUser(user.id),
})
// WHY IT LOOPS:
// 1. Query runs, gets data
// 2. Component re-renders
// 3. New `user` object created (different reference)
// 4. Key changes → query refetches
// 5. Goto 1 (infinite)
// CORRECT - use primitive values
useQuery({
queryKey: ['user', user.id], // ✅ String is stable
queryFn: () => fetchUser(user.id),
})Detection: React DevTools shows component re-rendering every frame. Network tab shows identical requests hammering server.
Why this is deceptively hard to debug: Page loads fine initially—first query succeeds. Then browser tab becomes unresponsive. CPU spikes to 100%. Network tab shows 50+ identical requests per second. React DevTools Profiler is unusable (too many renders). The cause—object reference in queryKey—is invisible in the network requests (they all look identical). Error isn't obvious: no stack trace, no warning, just performance death. Takes 10-15 minutes to realize it's React Query, then another 10-15 to isolate which query. Only after adding to every query do you see one logging hundreds of times. The fix (extract primitive from object) is obvious once found, but finding the culprit query in a codebase with 50+ queries is the hard part.
console.log问题:查询持续重取,浏览器冻结
typescript
// 错误 - 造成无限循环
useQuery({
queryKey: ['user', user], // ❌ 键中包含对象
queryFn: () => fetchUser(user.id),
})
// 循环原因:
// 1. 查询运行,获取数据
// 2. 组件重新渲染
// 3. 创建新的`user`对象(引用不同)
// 4. 键变更 → 查询重取
// 5. 回到步骤1(无限循环)
// 正确 - 使用原始值
useQuery({
queryKey: ['user', user.id], // ✅ 字符串是稳定引用
queryFn: () => fetchUser(user.id),
})检测方法:React DevTools显示组件每帧都在重新渲染。网络面板显示相同请求持续轰炸服务器。
调试难点:页面初始加载正常——首次查询成功。随后浏览器标签页失去响应,CPU占用率飙升至100%。网络面板显示每秒50+次相同请求。React DevTools分析器无法使用(渲染次数过多)。原因——queryKey中的对象引用——在网络请求中不可见(所有请求看起来都一样)。无堆栈跟踪、无警告,仅性能骤降。需要10-15分钟才能意识到是React Query的问题,再花10-15分钟定位具体查询。只有给每个查询添加,才能发现某个查询被调用数百次。找到问题后,修复方法(从对象中提取原始值)显而易见,但在拥有50+查询的代码库中定位问题查询是难点。
console.log❌ Stale Data Trap
❌ 过期数据陷阱
Problem: Data never updates despite changes on server
typescript
// WRONG - data stuck in cache
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: Infinity, // ❌ Never marks stale
})
// User adds todo on another tab → never sees it
// CORRECT - reasonable staleTime
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // ✅ 5 minutes
})Trade-off:
- → Refetch on every focus/mount (expensive)
staleTime: 0 - → Never refetch (stale data)
staleTime: Infinity - → Balance (refetch after 5min of inactivity)
staleTime: 5min
Why this is deceptively hard to debug: Works perfectly in development—you refresh constantly, clearing cache. In production, users keep tabs open for hours. They report "data doesn't update" but you can't reproduce (your dev habits differ). When you check DevTools, query shows fresh data (because you just opened DevTools, triggering window focus refetch if you have default settings). The user's is buried in a hook 3 files deep. No error, no warning. You check the API—returns fresh data. You check network—no requests being made (that's the clue, but easy to miss). Takes 20-30 minutes of user reproduction videos to notice they never see network requests after initial load. Only then do you search the codebase for settings.
staleTime: InfinitystaleTime问题:服务器数据已更新,但客户端数据从未刷新
typescript
// 错误 - 数据卡在缓存中
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: Infinity, // ❌ 永远不标记为过期
})
// 用户在另一个标签页添加待办事项 → 永远看不到更新
// 正确 - 合理的staleTime
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // ✅ 5分钟
})权衡:
- → 每次聚焦/挂载时重取(开销大)
staleTime: 0 - → 永远不重取(数据过期)
staleTime: Infinity - → 平衡方案(闲置5分钟后重取)
staleTime: 5min
调试难点:开发环境中工作正常——你会经常刷新,清除缓存。生产环境中,用户会保持标签页打开数小时。他们反馈“数据不更新”,但你无法复现(你的开发习惯不同)。当你检查DevTools时,查询显示最新数据(因为你刚打开DevTools,触发了窗口聚焦重取,如果使用默认设置的话)。用户的隐藏在3个文件之外的钩子中。无错误、无警告。你检查API——返回最新数据。你检查网络——无请求发出(这是线索,但容易被忽略)。需要20-30分钟分析用户复现视频,才能注意到初始加载后从未有网络请求。此时你才会在代码库中搜索设置。
staleTime: InfinitystaleTime❌ Over-Invalidation
❌ 过度失效
Problem: Unrelated data refetches on every mutation
typescript
// WRONG - nukes entire cache
onSuccess: () => {
queryClient.invalidateQueries() // ❌ Refetches EVERYTHING
}
// User updates profile → todos, posts, comments all refetch
// CORRECT - targeted invalidation
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] }) // ✅ Only user
}Why it hurts: 100 queries in cache → 100 network requests on every mutation. Kills mobile users.
问题:每次变更都会重取无关数据
typescript
// 错误 - 清空整个缓存
onSuccess: () => {
queryClient.invalidateQueries() // ❌ 重取所有数据
}
// 用户更新个人资料 → 待办事项、帖子、评论全部重取
// 正确 - 定向失效
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] }) // ✅ 仅重取用户数据
}危害:缓存中有100个查询 → 每次变更触发100次网络请求。严重影响移动端用户。
Decision Frameworks
决策框架
When to Use Optimistic Updates vs Invalidation
何时使用乐观更新 vs 失效
Mutation completes...
│
├─ Simple list append/prepend → Optimistic (useMutationState)
│ └─ Add todo, add comment, add item
│ WHY: No complex logic, just show pending item
│
├─ Complex computed data → Invalidation
│ └─ Change affects aggregates, filters, sorts
│ WHY: Server computes, client doesn't duplicate logic
│
├─ Risk of conflicts → Invalidation
│ └─ Multiple users editing same data
│ WHY: Optimistic update may be wrong, let server resolve
│
└─ Must feel instant → Optimistic + rollback on error
└─ Toggle like, toggle favorite
WHY: User expects immediate feedback变更完成后...
│
├─ 简单列表添加/前置 → 乐观更新(使用useMutationState)
│ └─ 添加待办事项、添加评论、添加条目
│ 原因:无复杂逻辑,仅显示待处理条目
│
├─ 复杂计算数据 → 失效
│ └─ 变更影响聚合、过滤、排序
│ 原因:由服务器计算,客户端无需重复逻辑
│
├─ 存在冲突风险 → 失效
│ └─ 多用户编辑同一数据
│ 原因:乐观更新可能不准确,让服务器解决冲突
│
└─ 必须即时反馈 → 乐观更新+错误回滚
└─ 切换点赞、切换收藏
原因:用户期望即时反馈React Query vs SWR
React Query vs SWR
Choose React Query when:
✅ Need fine-grained cache control (gc, stale times)
✅ Complex invalidation patterns
✅ Optimistic updates with rollback
✅ Infinite queries (pagination)
✅ Already using TanStack ecosystem (Table, Router)
Choose SWR when:
✅ Simpler API (less configuration)
✅ Automatic revalidation on focus is main use case
✅ Smaller bundle size priority
✅ Using Next.js (first-party support)Real-world: React Query wins for complex apps, SWR wins for simple dashboards.
选择React Query的场景:
✅ 需要细粒度缓存控制(gc、过期时间)
✅ 复杂的失效模式
✅ 带回滚的乐观更新
✅ 无限查询(分页)
✅ 已在使用TanStack生态(Table、Router)
选择SWR的场景:
✅ 更简洁的API(更少配置)
✅ 主要需求是聚焦时自动重验证
✅ 优先考虑更小的包体积
✅ 使用Next.js(官方原生支持)实际场景:React Query适合复杂应用,SWR适合简单仪表盘。
SSR Hydration Patterns (Next.js App Router)
SSR水合模式(Next.js App Router)
❌ Common Hydration Mismatch
❌ 常见水合不匹配
typescript
// WRONG - server renders loading, client renders data
function Page() {
const { data, isPending } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isPending) return <div>Loading...</div> // ❌ Mismatch
return <div>{data.map(...)}</div>
}
// SERVER: Renders "Loading..."
// CLIENT: Has cached data → renders list
// RESULT: Hydration errortypescript
// 错误 - 服务器渲染加载状态,客户端渲染数据
function Page() {
const { data, isPending } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isPending) return <div>加载中...</div> // ❌ 不匹配
return <div>{data.map(...)}</div>
}
// 服务器:渲染“加载中...”
// 客户端:有缓存数据 → 渲染列表
// 结果:水合错误✅ Correct Pattern with Prefetch
✅ 预取的正确模式
typescript
// app/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function Page() {
const queryClient = new QueryClient()
// Prefetch on server
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList /> {/* Client Component */}
</HydrationBoundary>
)
}
// components/TodoList.tsx (Client Component)
'use client'
export function TodoList() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// No isPending check - data guaranteed from server
return <div>{data.map(...)}</div>
}Why it works: Server prefetches, client hydrates with same data, no mismatch.
typescript
// app/page.tsx(服务器组件)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function Page() {
const queryClient = new QueryClient()
// 服务器端预取
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList /> {/* 客户端组件 */}
</HydrationBoundary>
)
}
// components/TodoList.tsx(客户端组件)
'use client'
export function TodoList() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 无需isPending检查 - 数据由服务器保证
return <div>{data.map(...)}</div>
}工作原理:服务器预取数据,客户端用相同数据水合,无匹配问题。
Debugging Commands
调试命令
Find refetch loops
查找重取循环
typescript
// Add to QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onSuccess: (data, query) => {
console.count(`Refetch: ${query.queryKey}`)
// If count > 10 in 1 second → infinite loop
}
}
}
})typescript
// 添加到QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onSuccess: (data, query) => {
console.count(`Refetch: ${query.queryKey}`)
// 如果1秒内计数>10 → 无限循环
}
}
}
})Visualize cache state
可视化缓存状态
typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
// Add to app
<ReactQueryDevtools initialIsOpen={false} />
// Click query → see refetch count, staleness, gc timetypescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
// 添加到应用
<ReactQueryDevtools initialIsOpen={false} />
// 点击查询 → 查看重取次数、过期状态、gc时间Test staleTime behavior
测试staleTime行为
typescript
// Force immediate stale (for testing)
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Check if query is stale
const state = queryClient.getQueryState(['todos'])
console.log(state?.isInvalidated) // true = will refetch on next mounttypescript
// 强制立即过期(用于测试)
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 检查查询是否过期
const state = queryClient.getQueryState(['todos'])
console.log(state?.isInvalidated) // true = 下次挂载时会重取Error Recovery Procedures
错误恢复流程
When Hydration Mismatch Occurs (SSR)
发生SSR水合不匹配时
Recovery steps:
- Verify data equality: Add in server and client components
console.log(JSON.stringify(data)) - Check prefetch completion: Inspect output—should contain queries object
dehydrate(queryClient) - Confirm queryKey match: Server and client must use EXACT same key (including array order)
- Fallback: If still mismatching, bypass prefetch and use from server props:
initialDatatypescript// Server passes data via props export default function Page() { const data = await fetchTodos() return <TodoList initialTodos={data} /> } // Client receives and uses as initialData function TodoList({ initialTodos }) { const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, initialData: initialTodos, // Hydrates without mismatch }) }
恢复步骤:
- 验证数据一致性:在服务器和客户端组件中添加
console.log(JSON.stringify(data)) - 检查预取完成情况:检查输出——应包含queries对象
dehydrate(queryClient) - 确认queryKey匹配:服务器和客户端必须使用完全相同的键(包括数组顺序)
- ** fallback方案**:如果仍不匹配,绕过预取,使用服务器props传递的:
initialDatatypescript// 服务器通过props传递数据 export default function Page() { const data = await fetchTodos() return <TodoList initialTodos={data} /> } // 客户端接收并作为initialData使用 function TodoList({ initialTodos }) { const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, initialData: initialTodos, // 水合时无匹配问题 }) }
When Infinite Refetch Loop Detected
检测到无限重取循环时
Recovery steps:
- Add refetch logging: Refetch: ${JSON.stringify(queryKey)}`)` in queryFn
console.count(\ - Identify the culprit: Check which count exceeds 10 in first 2 seconds
- Extract primitives from queryKey: Replace objects/arrays with IDs/strings
- Fallback: If key MUST contain object (rare), memoize it AND disable :
structuralSharingtypescriptconst stableKey = useMemo(() => ['user', user], [user.id]) useQuery({ queryKey: stableKey, queryFn: () => fetchUser(user.id), structuralSharing: false, // Prevents reference comparison issues })
恢复步骤:
- 添加重取日志:在queryFn中添加Refetch: ${JSON.stringify(queryKey)}`)`
console.count(\ - 定位问题查询:检查哪个计数在最初2秒内超过10
- 从queryKey中提取原始值:将对象/数组替换为ID/字符串
- ** fallback方案**:如果键必须包含对象(罕见),对其进行memoize并禁用:
structuralSharingtypescriptconst stableKey = useMemo(() => ['user', user], [user.id]) useQuery({ queryKey: stableKey, queryFn: () => fetchUser(user.id), structuralSharing: false, // 避免引用比较问题 })
When Stale Data Persists
过期数据持续存在时
Recovery steps:
- Check setting: Search codebase for
staleTimeor very large valuesstaleTime: Infinity - Force invalidation:
queryClient.invalidateQueries({ queryKey: ['your-key'] }) - Verify refetch: Check Network tab for request after invalidation
- Fallback: If data still stale, cache may be corrupted → clear and refetch:
Or nuclear option:typescript
queryClient.removeQueries({ queryKey: ['your-key'] }) queryClient.refetchQueries({ queryKey: ['your-key'] })(clears entire cache)queryClient.clear()
恢复步骤:
- 检查设置:在代码库中搜索
staleTime或极大值staleTime: Infinity - 强制失效:
queryClient.invalidateQueries({ queryKey: ['your-key'] }) - 验证重取:检查网络面板,确认失效后有请求发出
- ** fallback方案**:如果数据仍过期,缓存可能已损坏 → 清除并重取:
或终极方案:typescript
queryClient.removeQueries({ queryKey: ['your-key'] }) queryClient.refetchQueries({ queryKey: ['your-key'] })(清空整个缓存)queryClient.clear()
When v4→v5 Migration Breaks Silently
v4→v5迁移静默失效时
Recovery steps:
- Enable strict TypeScript: Add to
strict: trueto catch removed propertiestsconfig.json - Search for v4 property names:
grep -r "cacheTime\|isLoading\|keepPreviousData" src/ - Replace with v5 equivalents: Use find-replace for codebase-wide fixes
- Fallback: If TypeScript still doesn't catch it, add runtime warning in development:
typescript
// In QueryClient setup const queryClient = new QueryClient({ defaultOptions: { queries: { // @ts-ignore - intentionally check for v4 props ...(process.env.NODE_ENV === 'development' && { // Warn if someone passes v4 props onError: (err, query) => { if ('cacheTime' in query) console.warn('⚠️ cacheTime removed in v5, use gcTime') if ('isLoading' in query) console.warn('⚠️ isLoading removed in v5, use isPending') } }) } } })
恢复步骤:
- 启用严格TypeScript:在中添加
tsconfig.json,捕获已移除的属性strict: true - 搜索v4属性名:
grep -r "cacheTime\|isLoading\|keepPreviousData" src/ - 替换为v5等效属性:使用查找替换完成全代码库修复
- ** fallback方案**:如果TypeScript仍未捕获,在开发环境添加运行时警告:
typescript
// 在QueryClient设置中 const queryClient = new QueryClient({ defaultOptions: { queries: { // @ts-ignore - 故意检查v4属性 ...(process.env.NODE_ENV === 'development' && { // 如果有人传入v4属性则发出警告 onError: (err, query) => { if ('cacheTime' in query) console.warn('⚠️ cacheTime在v5中已移除,请使用gcTime') if ('isLoading' in query) console.warn('⚠️ isLoading在v5中已移除,请使用isPending') } }) } } })
When to Load Full Reference
何时查阅完整参考文档
MANDATORY - READ ENTIRE FILE: when:
references/v5-features.md- Using 3+ v5-specific features simultaneously (useMutationState, throwOnError, infinite queries)
- Need complete API reference for 5+ advanced hook options (select, placeholderData, notifyOnChangeProps)
- Implementing complex patterns (optimistic updates with rollback, parallel/dependent queries, suspense mode)
- Building custom hooks wrapping React Query with 4+ configuration options
MANDATORY - READ ENTIRE FILE: when:
references/migration-guide.md- Migrating codebase with 10+ query usages from v4 to v5
- Need exhaustive breaking changes checklist (20+ items to verify)
- Encountering 3+ different v4→v5 migration errors
- Setting up automated migration with codemods for large codebase (100+ queries)
Do NOT load references for:
- Single breaking change fix (use this core framework's Breaking Changes section)
- Basic troubleshooting (infinite loops, stale data, hydration—covered in core)
- Simple optimistic update (use Decision Frameworks section)
必须阅读全文:的场景:
references/v5-features.md- 同时使用3+个v5专属功能(useMutationState、throwOnError、无限查询)
- 需要5+个高级钩子选项(select、placeholderData、notifyOnChangeProps)的完整API参考
- 实现复杂模式(带回滚的乐观更新、并行/依赖查询、suspense模式)
- 构建封装React Query的自定义钩子,包含4+个配置选项
必须阅读全文:的场景:
references/migration-guide.md- 从v4迁移到v5,代码库中有10+个查询用法
- 需要完整的破坏性变更检查清单(20+项需验证)
- 遇到3+种不同的v4→v5迁移错误
- 为大型代码库(100+个查询)设置自动化迁移codemods
无需查阅参考文档的场景:
- 修复单个破坏性变更(使用本文的“破坏性变更”部分)
- 基础故障排查(无限循环、过期数据、水合问题——本文核心部分已覆盖)
- 简单乐观更新(使用本文的“决策框架”部分)
Resources
资源
- Official Docs: https://tanstack.com/query/latest (for API reference)
- This Skill: Production issues, migration gotchas, decision frameworks
- 官方文档:https://tanstack.com/query/latest(用于API参考)
- 本技能文档:生产环境问题、迁移陷阱、决策框架