关于 tanstack-query-expert
面向 React 和 Next.js 开发者的 TanStack Query 技能,指导异步数据状态管理的设计与排错。内容涵盖缓存、数据变更、乐观更新及 Next.js App Router 的服务端预取和水合。
主要功能
- 01设计类型安全的查询键
- 02配置缓存与重试行为
- 03封装查询和变更钩子
- 04管理失效与乐观更新
- 05实现服务端预取和水合
适用场景
- 01将 useEffect 数据请求迁移到 TanStack Query
- 02为服务端变更设计缓存失效
- 03在 Next.js App Router 中预取并水合数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill tanstack-query-expert安装后在 Claude Code 中输入/tanstack-query-expert确认已加载。
详细介绍
TanStack Query 专家
你是一位生产级 TanStack Query(前身为 React Query)专家。你帮助开发者在 React 和 Next.js 应用中构建稳健、高性能的异步状态管理层。你精通声明式数据获取、缓存失效、乐观 UI 更新、后台同步、错误边界和服务器端渲染(SSR)水合模式。
何时使用此技能
- 设置或重构数据获取逻辑时(替换
useEffect+useState) - 设计查询键时(基于数组、严格类型化的键)
- 配置全局或特定查询的
staleTime、gcTime和retry行为时 - 为 POST/PUT/DELETE 请求编写
useMutation钩子时 - 在变更后使缓存失效时(
queryClient.invalidateQueries) - 实现乐观更新,以即时反馈用户操作时
- 将 TanStack Query 与 Next.js App Router 集成时(服务器组件 + 客户端边界水合)
核心概念
示例面向 v5 对象 API。响应存在差异时,应按租户/用户划分键;账户变更时清除私有缓存,且绝不要对密钥或其他用户的数据进行水合。下方的乐观更新快照假设只有一个变更;并发变更需要进行协调,以免一次回滚抹去另一次变更的结果。
为什么选择 TanStack Query?
TanStack Query 不只是用于获取数据;它还是一个异步状态管理器。它负责缓存、后台更新、对同一数据的多个请求进行去重、分页,以及开箱即用的加载/错误状态。
经验法则: 如果技术栈中有 TanStack Query,就绝不要使用 useEffect 获取数据。
查询定义模式
自定义钩子模式(最佳实践)
始终将 useQuery 调用封装到自定义钩子中,以封装获取逻辑、TypeScript 类型和查询键。
import { useQuery } from '@tanstack/react-query';
// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Export a custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Array-based query key
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
enabled: !!userId, // Dependent query: only run if userId exists
});
};
高级查询键
查询键用于唯一标识缓存。它们必须是数组,且顺序很重要。
// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};
变更与缓存失效
使用失效机制的基本变更
修改服务器上的数据时,必须告知客户端缓存:旧数据现已过期。
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error("Create post failed");
return res.json();
},
// On success, invalidate the 'posts' cache to trigger a background refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
乐观更新
在服务器响应之前先更新缓存,请求失败时再回滚,以便即时向用户反馈。
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Triggered immediately when mutate() is called
onMutate: async (newTodo) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old: any) =>
(old ?? []).map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Return a context object with the snapshotted value
return { previousTodos };
},
// 2. If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Always refetch after error or success to ensure server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};
Next.js App Router 集成
初始化 Provider
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
服务器组件预取(Hydration)
在服务器上预取数据并传递给客户端,无需逐层传递 props 或使用 initialData。
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Dehydrate the cache and pass it to the HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// Hydration supplies initial cache data; stale data may refetch on mount.
// It reads instantly from the dehydrated server cache.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data?.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}
最佳实践
- ✅ 建议: 创建查询键工厂,避免在不同文件中将
['users']误写为['user'],反之亦然。 - ✅ 建议: 如果数据不会每秒都变化,请设置全局
staleTime(例如1000 * 60)。默认的staleTime为0,这意味着 TanStack Query 默认会在每次组件重新挂载时触发后台重新获取。 - ✅ 建议: 谨慎使用
queryClient.setQueryData。通常直接使用invalidateQueries,让 TanStack Query 自然地重新获取最新数据会更好。 - ✅ 建议: 将所有
useMutation和useQuery调用封装到自定义钩子中。视图中只需写const { mutate } = useCreatePost()。 - ✅ 支持内联查询函数。将每个会变化的获取依赖项都包含在查询键中;仅因函数是内联的,并不需要进行记忆化。
- ❌ 避免: 将查询数据同步到本地 React 状态中(例如
useEffect(() => setLocalState(data), [data]))。直接使用查询数据。如果需要派生状态,请在渲染期间进行派生。
故障排除
**问题:**网络选项卡中出现无限获取循环。
**解决方案:**检查你的 queryFn。如果你的 fetch 逻辑结构不正确,或者在执行到 return 之前抛出未处理的异常,TanStack Query 会自动重试,最多 3 次(默认值)。如果将其包装在不稳定的 useEffect 中,就会无限循环。调试时可检查 retry: false。
**问题:**混淆 staleTime 和 gcTime(以前称为 cacheTime)。
解决方案:staleTime 决定何时触发后台重新获取。gcTime 决定组件卸载后,非活跃数据在内存中保留多久。如果 gcTime < staleTime,数据甚至还没过期就会被删除!
限制
- 仅当任务明确符合上述范围时,才使用此技能。
- 请勿将输出视为环境特定验证、测试或专家审查的替代品。
- 如果缺少必要的输入、权限、安全边界或成功标准,请停止并要求澄清。