TanStack Query(前身为 React Query)是 React 应用中服务端状态管理的标准解决方案。它处理缓存、后台重新获取、去重、分页和无限查询——所有这些都通过简单的基于 hooks 的 API 实现。
useQuery:获取和缓存数据
useQuery 订阅服务端状态。queryKey 是缓存键——唯一标识数据的任何数组。staleTime 控制数据何时被视为过期并重新获取。
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Setup: wrap your app
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Basic query
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery<User>({
queryKey: ['user', userId], // cache key — must be unique
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: userId > 0, // only run if userId is valid
staleTime: 1000 * 60 * 10, // override global default
});
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return <div>{data.name} — {data.email}</div>;
}useMutation:创建、更新、删除
useMutation 处理创建/更新/删除操作。onSuccess 回调让你无效化相关查询或直接更新缓存而无需重新获取。
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreatePostInput {
title: string;
body: string;
userId: number;
}
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreatePostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onSuccess: (newPost) => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or add directly to cache (no refetch needed)
queryClient.setQueryData(['post', newPost.id], newPost);
},
onError: (error) => {
console.error('Failed to create post:', error);
},
});
}
function CreatePostForm() {
const mutation = useCreatePost();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
</form>
);
}乐观更新实现即时 UI
乐观更新在服务器响应之前立即将更改应用于 UI。如果请求失败,onError 回调会回滚到之前的状态。
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
function useToggleTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (todo: Todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !todo.completed }),
}).then(r => r.json()),
// Optimistic update — runs BEFORE the mutation
onMutate: async (updatedTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot current data for rollback
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// Optimistically update the cache
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo =>
todo.id === updatedTodo.id
? { ...todo, completed: !todo.completed }
: todo
)
);
return { previousTodos }; // context for onError
},
// If mutation fails, roll back to snapshot
onError: (_err, _variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}分页的无限查询
useInfiniteQuery 处理基于游标和偏移量的分页。getNextPageParam 从每个页面响应中提取下一页游标。
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Post[];
nextCursor?: string;
}
function useInfinitePosts() {
return useInfiniteQuery<Page>({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}`
: '/api/posts';
return fetch(url).then(r => r.json());
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<div>
{data?.pages.flatMap(page => page.items).map(post => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}查询键模式和预获取
一致的查询键工厂防止缓存不匹配。悬停时预获取创造即时页面转换——用户点击时数据已在缓存中。
// Query key patterns — factory functions for consistency
const queryKeys = {
all: ['posts'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
};
// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });
// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });
// Prefetching for better UX
async function prefetchUser(userId: number) {
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
});
}
// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
View Profile
</link>TanStack Query vs 其他解决方案
| Feature | TanStack Query | Redux Toolkit | SWR | Apollo |
|---|---|---|---|---|
| Caching | Built-in (staleTime, gcTime) | Manual | Built-in | Built-in (Apollo-only) |
| Optimistic updates | First-class | Manual | Basic | First-class |
| Infinite query | useInfiniteQuery | Manual | useSWRInfinite | fetchMore |
| Mutations | useMutation | RTK Query | No (use fetch) | useMutation |
| DevTools | Excellent | Excellent | Basic | Good |
| REST + GraphQL | Both | Both | REST focused | GraphQL only |
最佳实践
- 使用查询键工厂(带函数的对象)确保一致性并实现精确缓存失效。
- 根据数据新鲜度需求设置 staleTime。用户资料:10 分钟。股票价格:0 秒。参考数据:1 小时。
- 始终提供加载和错误状态。使用 React Suspense 模式配合 Suspense 边界编写更简洁的代码。
- 对切换操作、点赞等快速 mutations 使用乐观更新——使应用感觉即时响应。
- 在悬停或路由切换时预获取数据。TanStack Query 的 prefetchQuery 是幂等的——可以多次安全调用。
常见问题
staleTime 和 gcTime 有什么区别?
staleTime 控制缓存数据被认为是新鲜的时长(不需要重新获取)。gcTime 控制非活动缓存条目在垃圾回收前在内存中保存多长时间。
TanStack Query 如何处理竞态条件?
TanStack Query 自动去重相同的请求——如果 10 个组件同时请求同一查询键,只会触发 1 个网络请求。响应是共享的。
我应该将 TanStack Query 与 Redux 或 Zustand 一起使用吗?
TanStack Query 处理服务端状态(远程数据);Redux/Zustand 处理客户端状态(UI 状态、选择、表单草稿)。它们相互补充。
如何全局处理认证错误?
使用 QueryClient 的全局 onError 回调或自定义查询函数包装器。对于 401 错误,重定向到登录。
TanStack Query v5 有什么变化?
v5 将 cacheTime 重命名为 gcTime,使查询函数参数变为对象,删除了 useQuery 中的成功/错误/结算回调,并添加了对 React Server Components 的 SSR 支持。