tanstack-query by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill tanstack-query最后更新 : 2026-01-20 版本 : @tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2 要求 : React 18.0+ (useSyncExternalStore), TypeScript 4.7+ (推荐)
无需属性传递即可从任何地方访问突变状态:
import { useMutationState } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
// 获取所有待处理的突变
const pendingMutations = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables,
})
if (pendingMutations.length === 0) return null
return <div>正在保存 {pendingMutations.length} 个项目...</div>
}
// 按突变键过滤
const todoMutations = useMutationState({
filters: { mutationKey: ['addTodo'] },
})
使用 variables 的新模式 - 无需缓存操作,无需回滚:
function TodoList() {
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (newTodo) => api.addTodo(newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// 使用待处理突变中的变量显示乐观 UI
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
{/* 显示带视觉指示器的待处理项目 */}
{pendingTodos.map((todo, i) => (
<li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
))}
</ul>
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
从 useErrorBoundary 重命名(破坏性变更):
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<div>
错误! <button onClick={resetErrorBoundary}>重试</button>
</div>
)}>
<Todos />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // ✅ v5(v4 中是 useErrorBoundary)
})
return <div>{data.map(...)}</div>
}
控制离线时的行为:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 离线时使用缓存
},
},
})
// 按查询覆盖
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
networkMode: 'always', // 始终尝试,即使离线(适用于本地 API)
})
| 模式 | 行为 |
|---|---|
online(默认) | 仅在线时获取 |
always | 始终尝试(适用于本地/服务工作者 API) |
offlineFirst | 优先使用缓存,在线时获取 |
检测暂停状态:
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = 离线,等待网络
合并并行查询的结果:
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
error: results.find(r => r.error)?.error,
}),
})
// 访问合并结果
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]
无限查询的类型安全工厂(与 queryOptions 并行):
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'
const todosInfiniteOptions = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// 在钩子间复用
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)
限制无限查询在缓存中存储的页面数:
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor, // 使用 maxPages 时需要
maxPages: 3, // 仅在内存中保留 3 个页面
})
注意: maxPages 需要双向分页(getNextPageParam 和 getPreviousPageParam)。
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 分钟
gcTime: 1000 * 60 * 60, // 1 小时(v5:从 cacheTime 重命名)
refetchOnWindowFocus: false,
},
},
})
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
// 查询选项工厂(v5 模式)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// 用法:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
const { mutate } = useAddTodo()
if (isPending) return <div>加载中...</div>
if (isError) return <div>错误:{error.message}</div>
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}
✅ 对所有钩子使用对象语法
// v5 仅支持此语法:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
✅ 使用数组查询键
queryKey: ['todos'] // 列表
queryKey: ['todos', id] // 详情
queryKey: ['todos', { filter }] // 过滤
✅ 适当配置 staleTime
staleTime: 1000 * 60 * 5 // 5 分钟 - 防止过度重新获取
✅ 使用 isPending 表示初始加载状态
if (isPending) return <Loading />
// isPending = 尚无数据且正在获取
✅ 在 queryFn 中抛出错误
if (!response.ok) throw new Error('Failed')
✅ 突变后使查询失效
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
✅ 使用 queryOptions 工厂实现可复用模式
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
✅ 使用 gcTime(而非 cacheTime)
gcTime: 1000 * 60 * 60 // 1 小时
❌ 切勿使用 v4 数组/函数语法
// v4(v5 中已移除):
useQuery(['todos'], fetchTodos, options) // ❌
// v5(正确):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
❌ 切勿使用查询回调(查询中的 onSuccess、onError、onSettled)
// v5 已从查询中移除这些:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {}, // ❌ v5 中已移除
})
// 改用 useEffect:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// 执行某些操作
}
}, [data])
// 或使用突变回调(仍支持):
useMutation({
mutationFn: addTodo,
onSuccess: () => {}, // ✅ 对突变仍有效
})
❌ 切勿使用已弃用的选项
// v5 中已弃用:
cacheTime: 1000 // ❌ 改用 gcTime
isLoading: true // ❌ 含义已更改,使用 isPending
keepPreviousData: true // ❌ 改用 placeholderData
onSuccess: () => {} // ❌ 已从查询中移除
useErrorBoundary: true // ❌ 改用 throwOnError
❌ 切勿假设 isLoading 表示"尚无数据"
// v5 已更改此含义:
isLoading = isPending && isFetching // ❌ 现在表示"待处理且正在获取"
isPending = 尚无数据 // ✅ 使用此表示初始加载
❌ 切勿忘记无限查询的 initialPageParam
// v5 要求此参数:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ v5 中必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
❌ 切勿在 useSuspenseQuery 中使用 enabled
// 不允许:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ 不支持 Suspense
})
// 改用条件渲染:
{id && <TodoComponent id={id} />}
❌ 切勿依赖 refetchOnMount: false 来处理错误查询
// 无效 - 错误始终是过时的
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false, // ❌ 查询有错误时被忽略
})
// 改用 retryOnMount
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ 防止错误查询在挂载时重新获取
retry: 0,
})
此技能可预防 16 个已记录的问题,包括 v5 迁移、SSR/水合错误和常见错误:
错误:useQuery is not a function 或类型错误 来源:v5 迁移指南 发生原因:v5 移除了所有函数重载,仅支持对象语法 预防:始终使用 useQuery({ queryKey, queryFn, ...options })
之前(v4):
useQuery(['todos'], fetchTodos, { staleTime: 5000 })
之后(v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})
错误:回调不运行,TypeScript 错误 来源:v5 破坏性变更 发生原因:onSuccess、onError、onSettled 已从查询中移除(在突变中仍有效) 预防:使用 useEffect 处理副作用,或将逻辑移至突变回调
之前(v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Todos loaded:', data)
},
})
之后(v5):
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('Todos loaded:', data)
}
}, [data])
错误:UI 显示错误的加载状态 来源:v5 迁移:isLoading 重命名 发生原因:status: 'loading' 重命名为 status: 'pending',isLoading 含义已更改 预防:使用 isPending 表示初始加载,isLoading 表示"待处理且正在获取"
之前(v4):
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>
之后(v5):
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching(首次获取)
错误:cacheTime is not a valid option 来源:v5 迁移:gcTime 发生原因:重命名以更好地反映"垃圾回收时间" 预防:使用 gcTime 而非 cacheTime
之前(v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})
之后(v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})
错误:类型错误,enabled 选项不可用 来源:GitHub 讨论 #6206 发生原因:Suspense 保证数据可用,不能有条件地禁用 预防:使用条件渲染而非 enabled 选项
之前(v4/不正确):
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ 不允许
})
之后(v5/正确):
// 条件渲染:
{id ? (
<TodoComponent id={id} />
) : (
<div>未选择 ID</div>
)}
// 在 TodoComponent 内部:
function TodoComponent({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
// 无需 enabled 选项
})
return <div>{data.title}</div>
}
错误:initialPageParam is required 类型错误 来源:v5 迁移:无限查询 发生原因:v4 将 undefined 作为第一个 pageParam 传递,v5 需要显式值 预防:始终为无限查询指定 initialPageParam
之前(v4):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
之后(v5):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ 必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
错误:keepPreviousData is not a valid option 来源:v5 迁移:placeholderData 发生原因:被更灵活的 placeholderData 函数取代 预防:使用 placeholderData: keepPreviousData 辅助函数
之前(v4):
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})
之后(v5):
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})
错误:错误处理的类型错误 来源:v5 迁移:错误类型 发生原因:v4 使用 unknown,v5 默认使用 Error 类型 预防:如果抛出非 Error 类型,请显式指定错误类型
之前(v4 - 错误为 unknown):
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: unknown
之后(v5 - 指定自定义错误类型):
const { error } = useQuery<DataType, string>({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: string | null
// 或更好:始终抛出 Error 对象
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw new Error('custom error')
return data
},
})
// error: Error | null(默认)
错误:Hydration failed because the initial UI does not match what was rendered on the server 来源:GitHub Issue #9642 影响:v5.82.0+ 与流式 SSR(void prefetch 模式) 发生原因:hydrate() 同步解析但 query.fetch() 创建异步重试器的竞态条件,导致服务器和客户端之间的 isFetching/isStale 不匹配 预防:不要基于 fetchStatus 进行条件渲染(与 useSuspenseQuery 和流式预取一起使用),或者使用 await prefetch 而非 void 模式
之前(导致水合错误):
// 服务器:void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// 客户端:基于 fetchStatus 的条件渲染
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;
之后(解决方法):
// 选项 1:Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// 选项 2:不要基于 fetchStatus 渲染(与 Suspense 一起使用)
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>; // 不基于 isFetching 进行条件渲染
状态:已知问题,维护者正在调查。需要在 useSyncExternalStore 中实现 getServerSnapshot。
错误:水合期间的文本内容不匹配 来源:GitHub Issue #9399 影响:v5.x 与服务器端预取 发生原因:tryResolveSync 检测 RSC 有效负载中已解析的 promise 并在水合期间同步提取数据,绕过正常的待处理状态 预防:对 SSR 使用 useSuspenseQuery 而非 useQuery,或避免基于 isLoading 的条件渲染
之前(导致水合错误):
// 服务器组件
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// 客户端组件
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return <div>Loading...</div>; // 服务器渲染此内容
return <div>{data.length} todos</div>; // 客户端以此水合
}
之后(解决方法):
// 改用 useSuspenseQuery
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length} todos</div>;
}
状态:"在我的 OSS 待修复事项列表顶部" - 维护者 Ephem(2025 年 11 月)。需要在 useSyncExternalStore 中实现 getServerSnapshot。
错误:查询在挂载时重新获取,尽管设置了 refetchOnMount: false 来源:GitHub Issue #10018 影响:v5.90.16+ 发生原因:无数据的错误查询始终被视为过时。这是有意为之,以避免永久显示错误状态 预防:使用 retryOnMount: false 代替(或补充)refetchOnMount: false
之前(尽管设置仍会重新获取):
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('Fails') },
refetchOnMount: false, // 查询处于错误状态时被忽略
retry: 0,
});
// 每次组件挂载时查询都会重新获取
之后(正确):
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ 防止错误查询在挂载时重新获取
retry: 0,
});
状态:已记录的行为(有意为之)。名称 retryOnMount 略有误导 - 它控制错误查询是否在挂载时触发新获取,而非自动重试。
错误:突变回调中的 TypeScript 错误 来源:GitHub Issue #9660 影响:v5.89.0+ 发生原因:在 variables 和 context 之间添加了 onMutateResult 参数,将回调签名从 3 个参数更改为 4 个 预防:更新所有突变回调以接受 4 个参数而非 3 个
之前(v5.88 及更早版本):
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context 现在是 onMutateResult,缺少最终的 context 参数
},
onSuccess: (data, variables, context) => {
// 相同问题
}
});
之后(v5.89.0+):
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = 来自 onMutate 的返回值
// context = 突变函数上下文
},
onSuccess: (data, variables, onMutateResult, context) => {
// 具有 4 个参数的正确签名
}
});
注意:如果不使用 onMutate,onMutateResult 参数将为 undefined。此破坏性变更在补丁版本中引入。
错误:Type 'readonly ["todos", string]' is not assignable to type '["todos", string]' 来源:GitHub Issue #9871 | 在 PR #9872 中修复 影响:仅 v5.90.8(在 v5.90.9 中修复) 发生原因:部分查询匹配破坏了只读查询键的 TypeScript 类型(使用 as const) 预防:升级到 v5.90.9+,或如果停留在 v5.90.8 则使用类型断言
之前(v5.90.8 - TypeScript 错误):
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// 类型:readonly ['todos', string] | readonly ['todos']
useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// 错误:readonly ['todos', string] 不可分配给 ['todos', string]
});
}
});
之后(v5.90.9+):
// 与只读类型正确工作
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ 无类型错误
});
状态:在 v5.90.9 中修复。特别影响使用代码生成器(如 openapi-react-query)生成只读查询键的用户。
错误:mutation.state.variables 类型为 unknown 而非实际类型 来源:GitHub Issue #9825 影响:所有 v5.x 版本 发生原因:模糊的突变键匹配阻止了保证的类型推断(与 queryClient.getQueryCache().find() 相同的问题) 预防:在 select 回调中显式转换类型
之前(类型推断无效):
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // 类型:unknown
},
});
之后(显式转换):
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// 或转换整个状态:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>
状态:模糊匹配的已知限制。无修复计划。
错误:使用 fetchQuery() 与 useQuery 时出现 CancelledError 来源:GitHub Issue #9798 影响:仅开发环境(React StrictMode) 发生原因:StrictMode 导致双重挂载/卸载。当 useQuery 卸载且是最后一个观察者时,即使 fetchQuery() 也在运行,它也会取消查询 预防:这是预期的仅开发环境行为。不影响生产环境
示例:
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('Loaded:', data); // 在 StrictMode 中从不记录
} catch (error) {
console.error('Failed:', error); // CancelledError
}
}
function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// 在 StrictMode 中,组件卸载/重新挂载,取消 fetchQuery
}
解决方法:
// 使用 staleTime 保持查询被观察
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // 保持查询活动
});
状态:预期的 StrictMode 行为,非错误。生产构建不受影响。
错误:尽管调用了 invalidateQueries(),非活动查询未重新获取 来源:GitHub Issue #9531 影响:所有 v5.x 版本 发生原因:文档有误导性 - invalidateQueries() 默认仅重新获取"活动"查询,而非"所有"查询 预防:使用 refetchType: 'all' 强制重新获取非活动查询
默认行为:
// 仅活动查询(当前被观察的)会重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
要重新获取非活动查询:
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // 重新获取活动和非活动查询
});
状态:文档已修复以澄清"活动"查询。这是预期行为。
注意:这些技巧来自社区专家和维护者博客。请根据您的版本进行验证。
来源:TkDodo 的博客 - API 设计经验教训 | 置信度:高 适用于:v5.27.3+
当多个组件使用具有不同选项(如 staleTime)的相同查询时,"最后写入获胜"规则适用于未来的获取,但当前进行中的查询使用其原始选项。当组件在不同时间挂载时,这可能导致意外行为。
意外行为示例:
// 组件 A 首先挂载
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // 初始应用
});
}
// 组件 B 在 A 的查询进行中时挂载
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // 不影响当前获取,仅影响未来的
});
}
推荐方法:
// 将选项编写为引用最新值的函数
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // 每次渲染时评估
});
来源:避免使用 TanStack Query 的常见错误 | 置信度:高
refetch() 函数应仅用于使用相同参数刷新(如手动"重新加载"按钮)。对于新参数(过滤器、页码、搜索词等),请将它们包含在查询键中。
反模式:
// ❌ 错误 - 使用 refetch() 处理不同参数
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // 所有页面使用相同键
queryFn: () => fetchTodos(page),
});
// 这会使用旧的页面值重新获取,而非新的
<button onClick={() => {
Last Updated : 2026-01-20 Versions : @tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2 Requires : React 18.0+ (useSyncExternalStore), TypeScript 4.7+ (recommended)
Access mutation state from anywhere without prop drilling:
import { useMutationState } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
// Get all pending mutations
const pendingMutations = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables,
})
if (pendingMutations.length === 0) return null
return <div>Saving {pendingMutations.length} items...</div>
}
// Filter by mutation key
const todoMutations = useMutationState({
filters: { mutationKey: ['addTodo'] },
})
New pattern using variables - no cache manipulation, no rollback needed:
function TodoList() {
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (newTodo) => api.addTodo(newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// Show optimistic UI using variables from pending mutations
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
{/* Show pending items with visual indicator */}
{pendingTodos.map((todo, i) => (
<li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
))}
</ul>
)
}
Renamed from useErrorBoundary (breaking change):
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<div>
Error! <button onClick={resetErrorBoundary}>Retry</button>
</div>
)}>
<Todos />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // ✅ v5 (was useErrorBoundary in v4)
})
return <div>{data.map(...)}</div>
}
Control behavior when offline:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // Use cache when offline
},
},
})
// Per-query override
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
networkMode: 'always', // Always try, even offline (for local APIs)
})
| Mode | Behavior |
|---|---|
online (default) | Only fetch when online |
always | Always try (useful for local/service worker APIs) |
offlineFirst | Use cache first, fetch when online |
Detecting paused state:
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network
Combine results from parallel queries:
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
error: results.find(r => r.error)?.error,
}),
})
// Access combined result
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]
Type-safe factory for infinite queries (parallel to queryOptions):
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'
const todosInfiniteOptions = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Reuse across hooks
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)
Limit pages stored in cache for infinite queries:
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages
maxPages: 3, // Only keep 3 pages in memory
})
Note: maxPages requires bi-directional pagination (getNextPageParam AND getPreviousPageParam).
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 min
gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)
refetchOnWindowFocus: false,
},
},
})
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
// Query options factory (v5 pattern)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// Usage:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
const { mutate } = useAddTodo()
if (isPending) return <div>Loading...</div>
if (isError) return <div>Error: {error.message}</div>
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}
✅ Use object syntax for all hooks
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
✅ Use array query keys
queryKey: ['todos'] // List
queryKey: ['todos', id] // Detail
queryKey: ['todos', { filter }] // Filtered
✅ Configure staleTime appropriately
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches
✅ Use isPending for initial loading state
if (isPending) return <Loading />
// isPending = no data yet AND fetching
✅ Throw errors in queryFn
if (!response.ok) throw new Error('Failed')
✅ Invalidate queries after mutations
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
✅ Use queryOptions factory for reusable patterns
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
✅ Use gcTime (not cacheTime)
gcTime: 1000 * 60 * 60 // 1 hour
❌ Never use v4 array/function syntax
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌
// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
❌ Never use query callbacks (onSuccess, onError, onSettled in queries)
// v5 removed these from queries:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {}, // ❌ Removed in v5
})
// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// Do something
}
}, [data])
// Or use mutation callbacks (still supported):
useMutation({
mutationFn: addTodo,
onSuccess: () => {}, // ✅ Still works for mutations
})
❌ Never use deprecated options
// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead
❌ Never assume isLoading means "no data yet"
// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load
❌ Never forget initialPageParam for infinite queries
// v5 requires this:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
❌ Never use enabled with useSuspenseQuery
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})
// Use conditional rendering instead:
{id && <TodoComponent id={id} />}
❌ Never rely on refetchOnMount: false for errored queries
// Doesn't work - errors are always stale
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false, // ❌ Ignored when query has error
})
// Use retryOnMount instead
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch for errored queries
retry: 0,
})
This skill prevents 16 documented issues from v5 migration, SSR/hydration bugs, and common mistakes:
Error : useQuery is not a function or type errors Source : v5 Migration Guide Why It Happens : v5 removed all function overloads, only object syntax works Prevention : Always use useQuery({ queryKey, queryFn, ...options })
Before (v4):
useQuery(['todos'], fetchTodos, { staleTime: 5000 })
After (v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})
Error : Callbacks don't run, TypeScript errors Source : v5 Breaking Changes Why It Happens : onSuccess, onError, onSettled removed from queries (still work in mutations) Prevention : Use useEffect for side effects, or move logic to mutation callbacks
Before (v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Todos loaded:', data)
},
})
After (v5):
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('Todos loaded:', data)
}
}, [data])
Error : UI shows wrong loading state Source : v5 Migration: isLoading renamed Why It Happens : status: 'loading' renamed to status: 'pending', isLoading meaning changed Prevention : Use isPending for initial load, isLoading for "pending AND fetching"
Before (v4):
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>
After (v5):
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)
Error : cacheTime is not a valid option Source : v5 Migration: gcTime Why It Happens : Renamed to better reflect "garbage collection time" Prevention : Use gcTime instead of cacheTime
Before (v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})
After (v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})
Error : Type error, enabled option not available Source : GitHub Discussion #6206 Why It Happens : Suspense guarantees data is available, can't conditionally disable Prevention : Use conditional rendering instead of enabled option
Before (v4/incorrect):
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not allowed
})
After (v5/correct):
// Conditional rendering:
{id ? (
<TodoComponent id={id} />
) : (
<div>No ID selected</div>
)}
// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
// No enabled option needed
})
return <div>{data.title}</div>
}
Error : initialPageParam is required type error Source : v5 Migration: Infinite Queries Why It Happens : v4 passed undefined as first pageParam, v5 requires explicit value Prevention : Always specify initialPageParam for infinite queries
Before (v4):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
After (v5):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Error : keepPreviousData is not a valid option Source : v5 Migration: placeholderData Why It Happens : Replaced with more flexible placeholderData function Prevention : Use placeholderData: keepPreviousData helper
Before (v4):
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})
After (v5):
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})
Error : Type errors with error handling Source : v5 Migration: Error Types Why It Happens : v4 used unknown, v5 defaults to Error type Prevention : If throwing non-Error types, specify error type explicitly
Before (v4 - error was unknown):
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: unknown
After (v5 - specify custom error type):
const { error } = useQuery<DataType, string>({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: string | null
// Or better: always throw Error objects
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw new Error('custom error')
return data
},
})
// error: Error | null (default)
Error : Hydration failed because the initial UI does not match what was rendered on the server Source : GitHub Issue #9642 Affects : v5.82.0+ with streaming SSR (void prefetch pattern) Why It Happens : Race condition where hydrate() resolves synchronously but query.fetch() creates async retryer, causing isFetching/isStale mismatch between server and client Prevention : Don't conditionally render based on fetchStatus with useSuspenseQuery and streaming prefetch, OR await prefetch instead of void pattern
Before (causes hydration error):
// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;
After (workaround):
// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>; // No conditional on isFetching
Status : Known issue, being investigated by maintainers. Requires implementation of getServerSnapshot in useSyncExternalStore.
Error : Text content mismatch during hydration Source : GitHub Issue #9399 Affects : v5.x with server-side prefetching Why It Happens : tryResolveSync detects resolved promises in RSC payload and extracts data synchronously during hydration, bypassing normal pending state Prevention : Use useSuspenseQuery instead of useQuery for SSR, or avoid conditional rendering based on isLoading
Before (causes hydration error):
// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Client Component
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return <div>Loading...</div>; // Server renders this
return <div>{data.length} todos</div>; // Client hydrates with this
}
After (workaround):
// Use useSuspenseQuery instead
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length} todos</div>;
}
Status : "At the top of my OSS list of things to fix" - maintainer Ephem (Nov 2025). Requires implementing getServerSnapshot in useSyncExternalStore.
Error : Queries refetch on mount despite refetchOnMount: false Source : GitHub Issue #10018 Affects : v5.90.16+ Why It Happens : Errored queries with no data are always treated as stale. This is intentional to avoid permanently showing error states Prevention : Use retryOnMount: false instead of (or in addition to) refetchOnMount: false
Before (refetches despite setting):
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('Fails') },
refetchOnMount: false, // Ignored when query is in error state
retry: 0,
});
// Query refetches every time component mounts
After (correct):
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch on mount for errored queries
retry: 0,
});
Status : Documented behavior (intentional). The name retryOnMount is slightly misleading - it controls whether errored queries trigger a new fetch on mount, not automatic retries.
Error : TypeScript errors in mutation callbacks Source : GitHub Issue #9660 Affects : v5.89.0+ Why It Happens : onMutateResult parameter added between variables and context, changing callback signatures from 3 params to 4 Prevention : Update all mutation callbacks to accept 4 parameters instead of 3
Before (v5.88 and earlier):
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context is now onMutateResult, missing final context param
},
onSuccess: (data, variables, context) => {
// Same issue
}
});
After (v5.89.0+):
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = return value from onMutate
// context = mutation function context
},
onSuccess: (data, variables, onMutateResult, context) => {
// Correct signature with 4 parameters
}
});
Note : If you don't use onMutate, the onMutateResult parameter will be undefined. This breaking change was introduced in a patch version.
Error : Type 'readonly ["todos", string]' is not assignable to type '["todos", string]' Source : GitHub Issue #9871 | Fixed in PR #9872 Affects : v5.90.8 only (fixed in v5.90.9) Why It Happens : Partial query matching broke TypeScript types for readonly query keys (using as const) Prevention : Upgrade to v5.90.9+ or use type assertions if stuck on v5.90.8
Before (v5.90.8 - TypeScript error):
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']
useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// Error: readonly ['todos', string] not assignable to ['todos', string]
});
}
});
After (v5.90.9+):
// Works correctly with readonly types
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ No type error
});
Status : Fixed in v5.90.9. Particularly affected users of code generators like openapi-react-query that produce readonly query keys.
Error : mutation.state.variables typed as unknown instead of actual type Source : GitHub Issue #9825 Affects : All v5.x versions Why It Happens : Fuzzy mutation key matching prevents guaranteed type inference (same issue as queryClient.getQueryCache().find()) Prevention : Explicitly cast types in the select callback
Before (type inference doesn't work):
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // Type: unknown
},
});
After (with explicit cast):
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>
Status : Known limitation of fuzzy matching. No planned fix.
Error : CancelledError when using fetchQuery() with useQuery Source : GitHub Issue #9798 Affects : Development only (React StrictMode) Why It Happens : StrictMode causes double mount/unmount. When useQuery unmounts and is the last observer, it cancels the query even if fetchQuery() is also running Prevention : This is expected development-only behavior. Doesn't affect production
Example:
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('Loaded:', data); // Never logs in StrictMode
} catch (error) {
console.error('Failed:', error); // CancelledError
}
}
function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// In StrictMode, component unmounts/remounts, cancelling fetchQuery
}
Workaround:
// Keep query observed with staleTime
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // Keeps query active
});
Status : Expected StrictMode behavior, not a bug. Production builds are unaffected.
Error : Inactive queries not refetching despite invalidateQueries() call Source : GitHub Issue #9531 Affects : All v5.x versions Why It Happens : Documentation was misleading - invalidateQueries() only refetches "active" queries by default, not "all" queries Prevention : Use refetchType: 'all' to force refetch of inactive queries
Default behavior:
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
To refetch inactive queries:
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // Refetch active AND inactive
});
Status : Documentation fixed to clarify "active" queries. This is the intended behavior.
Note : These tips come from community experts and maintainer blogs. Verify against your version.
Source : TkDodo's Blog - API Design Lessons | Confidence : HIGH Applies to : v5.27.3+
When multiple components use the same query with different options (like staleTime), the "last write wins" rule applies for future fetches, but the current in-flight query uses its original options. This can cause unexpected behavior when components mount at different times.
Example of unexpected behavior:
// Component A mounts first
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Applied initially
});
}
// Component B mounts while A's query is in-flight
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // Won't affect current fetch, only future ones
});
}
Recommended approach:
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // Evaluated on each render
});
Source : Avoiding Common Mistakes with TanStack Query | Confidence : HIGH
The refetch() function should ONLY be used for refreshing with the same parameters (like a manual "reload" button). For new parameters (filters, page numbers, search terms, etc.), include them in the query key instead.
Anti-pattern:
// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // Same key for all pages
queryFn: () => fetchTodos(page),
});
// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>
Correct pattern:
// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page], // Key changes with page
queryFn: () => fetchTodos(page),
// Query automatically refetches when page changes
});
<button onClick={() => setPage(2)}>Next</button> // Just update state
When to use refetch():
// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
<button onClick={() => refetch()}>Refresh</button> // Same parameters
Dependent Queries (Query B waits for Query A):
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})
Parallel Queries (fetch multiple at once):
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})
Prefetching (preload on hover):
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
Infinite Scroll (useInfiniteQuery):
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Query Cancellation (auto-cancel on queryKey change):
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos?q=${search}`, { signal })
return res.json()
}
Data Transformation (select):
select: (data) => data.filter(todo => todo.completed)
Avoid Request Waterfalls : Fetch in parallel when possible (don't chain queries unless truly dependent)
Official Docs : https://tanstack.com/query/latest | v5 Migration : https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub : https://github.com/TanStack/query | Context7 : /websites/tanstack_query
Weekly Installs
2.5K
Repository
GitHub Stars
661
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode1.7K
claude-code1.6K
codex1.6K
gemini-cli1.5K
github-copilot1.4K
cursor1.2K
Flutter应用架构设计指南:分层结构、数据层实现与最佳实践
3,000 周安装
Rust调用关系图生成器 - 可视化函数调用层次结构,提升代码分析效率
539 周安装
parallel-web-extract:并行网页内容提取工具,高效抓取网页数据
595 周安装
腾讯云CloudBase AI模型Web技能:前端调用混元/DeepSeek模型,实现流式文本生成
560 周安装
Apollo Connectors 模式助手:GraphQL API 连接器开发与集成指南
565 周安装
GitHub Trending 趋势分析工具:实时发现热门项目、技术洞察与开源机会
556 周安装
GSAP React 集成教程:useGSAP Hook 动画库与 React 组件开发指南
546 周安装