关于 tanstack-query
这项技能介绍如何在 TypeScript 应用中使用 TanStack Query v5 管理服务端状态与缓存,侧重类型安全查询、缓存操作、变更处理及避免过时的 v4 API。
主要功能
- 01稳定的层级查询键
- 02可复用的类型化 queryOptions
- 03依赖查询与并行查询
- 04变更操作与缓存失效
- 05Suspense 和分页查询
- 06v5 迁移指南
适用场景
- 01在多个组件间组织查询定义
- 02获取依赖数据或并行数据
- 03写入后保持缓存一致
- 04实现分页数据加载
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mindrally/ skills --skill tanstack-query安装后在 Claude Code 中输入/tanstack-query确认已加载。
详细介绍
TanStack Query 最佳实践
TanStack Query(以前称为 React Query)开箱即用地处理服务端状态缓存、后台更新和过期数据管理。本指南介绍 v5 的模式和 API——v5 引入了多项相较于 v4 的重大变更,网上的旧示例仍未反映这些变化。
核心原则
- 使用 TanStack Query 管理所有服务端状态和数据获取;它不是通用的客户端状态管理器——请将仅限客户端的状态保存在
useState、context 或状态库中 - 减少使用
useEffect和useState管理服务端数据;优先使用 TanStack Query 内置的状态管理功能 - 每个查询都需要稳定、可序列化的查询键,用于唯一描述其所持有的数据
- 写操作由 mutations 处理,读操作由 queries 处理——不要混淆两者
- 实施完善的错误处理,并提供用户易于理解的提示
- 使用 TypeScript 确保查询响应具有完整的类型安全性
需要注意的 v5 重大变更
如果看到或编写了以下任何 v4 模式,请进行更新:
- 仅支持对象语法:
useQuery、useInfiniteQuery等不再接受位置参数(useQuery(key, fn, options))。始终传入单个 options 对象:useQuery({ queryKey, queryFn, ...options })。 isPending取代isLoading,用于表示useMutation中“尚无数据且正在执行获取”的状态。对于useQuery,isPending表示完全没有缓存数据;现在isLoading是派生状态(isPending && isFetching),仍可用于经典的“首次加载”加载指示器场景。cacheTime更名为gcTime(垃圾回收时间)。- 用于定义可复用、类型化查询定义的
queryOptions()辅助函数,可供组件、加载器和预取调用共享。 - 基于 Suspense 获取数据时使用
useSuspenseQuery(以及useSuspenseInfiniteQuery),取代旧的suspense: true选项。 - 分页时使用
placeholderData: keepPreviousData,取代旧的keepPreviousData: true布尔值。
项目结构
src/
api/
client.ts # API client configuration
endpoints/
users.ts # User-related API calls
posts.ts # Post-related API calls
queries/
postKeys.ts # Query key factory
postQueryOptions.ts # queryOptions() definitions
hooks/
queries/
useUsers.ts # User query hooks
usePosts.ts # Post query hooks
mutations/
useCreateUser.ts # User mutation hooks
providers/
QueryProvider.tsx # Query client provider setup
types/
api.ts # API response types
设置和配置
Query Client 配置
// providers/QueryProvider.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 minutes; defaults to 0 (always stale) if unset
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: (failureCount, error: any) => error?.status !== 404 && failureCount < 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
mutations: {
retry: 1,
},
},
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
在应用根部只实例化一次 QueryClient——不要在组件内部实例化,否则缓存会在每次渲染时重置。
查询最佳实践
1. 查询键组织
使用一致的分层查询键,以便高效管理缓存:
// Query key factory pattern
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
2. queryOptions 辅助函数(v5)
使用 queryOptions() 定义一次查询,并在组件、路由加载器和预取调用中复用同一定义——这样查询键、查询函数和选项就能集中在一处,而不必重复编写:
// queries/postQueryOptions.ts
import { queryOptions } from '@tanstack/react-query';
import { postKeys } from './postKeys';
import { fetchPost } from '@/api/endpoints/posts';
export const postQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5,
});
// In a component
const { data } = useQuery(postQueryOptions(postId));
// In a TanStack Router loader — eliminates loading spinners on navigation
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params, context: { queryClient } }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId)),
});
始终在组件外定义 queryOptions——不要在每次 useQuery() 调用中内联创建新的对象字面量——这样才能共享和预取该定义。
3. 自定义查询 Hook
当 queryOptions() 工厂函数不会在其他地方复用时,可创建可复用、类型化的查询 Hook:
// hooks/queries/useUser.ts
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { userKeys } from '@/api/queryKeys';
import { getUser, User } from '@/api/endpoints/users';
export function useUser(
userId: string,
options?: Omit<UseQueryOptions<User, Error>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => getUser(userId),
enabled: !!userId,
...options,
});
}
4. 依赖查询
处理依赖其他数据的查询:
function useUserPosts(userId: string) {
const { data: user } = useUser(userId);
return useQuery({
queryKey: ['posts', { userId }],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Only run when user data is available
});
}
5. 并行查询
同时获取多个资源:
import { useQueries } from '@tanstack/react-query';
function useMultipleUsers(userIds: string[]) {
return useQueries({
queries: userIds.map((id) => ({
queryKey: userKeys.detail(id),
queryFn: () => getUser(id),
})),
});
}
Mutation 最佳实践
1. 基本 Mutations
const { mutate, mutateAsync, isPending } = useMutation({
mutationFn: (input: CreatePostInput) => createPost(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
toast.success('Post created!');
},
onError: (error) => {
toast.error(error.message);
},
});
// Usage
mutate({ title: 'Hello', body: '...' });
isPending 是 v5 中表示“mutation 正在执行”的名称(v4 中 mutations 也使用 isLoading 表示此状态——该名称已废弃)。
2. 乐观更新
在 mutations 执行期间提供即时反馈:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) });
// Snapshot previous value
const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id));
// Optimistically update
queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
userKeys.detail(newUser.id),
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
},
});
}
3. 缓存失效
在 mutations 后正确地使相关查询失效:
function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteUser,
onSuccess: () => {
// Invalidate all user-related queries
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
});
}
其他值得了解的缓存操作:
// Remove from cache entirely (not just marked stale)
queryClient.removeQueries({ queryKey: userKeys.detail(id) });
// Directly write to the cache without a refetch
queryClient.setQueryData(userKeys.detail(id), newData);
错误处理
1. 全局错误处理器
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: false,
},
mutations: {
onError: (error) => {
// Global error handling (e.g., toast notification)
toast.error(error.message);
},
},
},
});
2. 组件级错误处理
function UserProfile({ userId }: { userId: string }) {
const { data, error, isPending, isError } = useUser(userId);
if (isPending) return <Skeleton />;
if (isError) return <ErrorMessage error={error} />;
return <UserCard user={data} />;
}
3. 条件重试逻辑
对于重试也不会成功的错误(例如 404),跳过重试:
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
4. Suspense 模式(v5)
使用 useSuspenseQuery 进行基于 Suspense 的数据获取,而不是旧的 suspense: true 选项——它还会收窄返回类型,因为 data 永远不会是 undefined:
import { useSuspenseQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
function UserProfile({ userId }: { userId: string }) {
// No need to check isPending — Suspense handles the loading state
const { data } = useSuspenseQuery(userQueryOptions(userId));
return <UserCard user={data} />;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
);
}
如果你希望错误冒泡到最近的 ErrorBoundary,但不切换到 Suspense,可在常规 useQuery 上设置 throwOnError: true。
性能优化
1. 选择并转换数据
只订阅所需的数据:
function useUserName(userId: string) {
return useUser(userId, {
select: (user) => user.name,
});
}
2. 预取
提前预取数据——可以在悬停时或路由过程中进行:
function UserList() {
const queryClient = useQueryClient();
const prefetchUser = (userId: string) => {
queryClient.prefetchQuery(userQueryOptions(userId));
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
3. 无限查询
高效处理分页数据:
import { useInfiniteQuery } from '@tanstack/react-query';
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: postKeys.lists(),
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
}
// data.pages is an array of page results — flatten for rendering
const allPosts = data?.pages.flatMap((page) => page.items) ?? [];
在分页或筛选查询上使用 placeholderData: keepPreviousData(从 @tanstack/react-query 导入),在加载下一页时继续显示上一页的数据,而不是闪现加载状态:
import { keepPreviousData, useQuery } from '@tanstack/react-query';
useQuery({
queryKey: postKeys.list({ page }),
queryFn: () => fetchPosts({ page }),
placeholderData: keepPreviousData,
});
使用 notifyOnChangeProps 将重新渲染限制为组件实际读取的特定结果属性。
TypeScript 技巧
- 始终显式标注
queryFn的返回值类型,或从有类型的 API 函数推断 - 使用
QueryObserverResult<TData, TError>标注钩子返回值的类型 - 对 mutation 使用
UseMutationResult<TData, TError, TVariables>
关键约定
- 按功能组织:在特定功能目录中归类查询钩子和
queryOptions工厂函数 - 保持查询键一致:使用工厂函数创建类型安全且组织有序的查询键
- 可复用时始终使用 queryOptions:只要某个查询会在多个位置使用(组件、loader、预取),就优先使用
queryOptions(),而不是临时内联选项 - 类型安全:为所有 API 响应定义 TypeScript 接口
- DevTools:始终在开发环境中加入 React Query DevTools
- 避免查询嵌套过深:尽可能扁平化查询结构
- 只获取所需数据:使用 API 参数限制响应大小
- 处理加载和错误状态:始终提供适当的 UI 反馈
应避免的反模式
- 不要使用
useEffect获取数据——改用查询或路由加载器 - 不要将服务端状态存储在本地状态中(
useState) - 不要向
useQuery/useInfiniteQuery传递位置参数——v5 要求使用选项对象形式 - 不要只检查 mutation 的
isLoading——应使用isPending - 不要忘记处理加载和错误状态
- 不要创建过于具体的查询键,以免妨碍缓存复用
- 不要在 mutation 后跳过缓存失效处理
- 条件查询时不要忽略
enabled选项 - 如果会在其他地方复用,不要在组件内联定义
queryOptions/查询配置——应将其放在一起并共享