tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query Patterns
TanStack Query 模式
Purpose
目的
Modern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.
Note: v5 (released October 2023) has breaking changes from v4:
- →
isLoadingfor statusisPending - →
cacheTime(garbage collection time)gcTime - React 18.0+ required
- Callbacks removed from useQuery (onError, onSuccess, onSettled)
- replaced with
keepPreviousDatafunctionplaceholderData
使用TanStack Query v5(最新版本:5.90.5,2025年11月)实现现代化数据获取,重点介绍基于Suspense的查询、缓存优先策略以及集中式API服务。
注意:v5(2023年10月发布)相比v4存在破坏性变更:
- 状态字段 变更为
isLoadingisPending - 变更为
cacheTime(垃圾回收时间)gcTime - 要求React 18.0及以上版本
- 移除useQuery中的回调函数(onError、onSuccess、onSettled)
- 被
keepPreviousData函数替代placeholderData
When to Use This Skill
适用场景
- Fetching data with TanStack Query
- Using useSuspenseQuery or useQuery
- Managing mutations
- Cache invalidation and updates
- API service patterns
- 使用TanStack Query获取数据
- 使用useSuspenseQuery或useQuery
- 管理Mutation操作
- 缓存失效与更新
- API服务模式设计
Quick Start
快速开始
Primary Pattern: useSuspenseQuery
核心模式:useSuspenseQuery
For all new components, use :
useSuspenseQuerytypescript
import { useSuspenseQuery } from '@tanstack/react-query';
import { postsApi } from '~/features/posts/api/postsApi';
function PostList() {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// Wrap with Suspense
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>Benefits:
- No checks needed
isLoading - Integrates with Suspense boundaries
- Cleaner component code
- Consistent loading UX
对于所有新组件,优先使用:
useSuspenseQuerytypescript
import { useSuspenseQuery } from '@tanstack/react-query';
import { postsApi } from '~/features/posts/api/postsApi';
function PostList() {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// 用Suspense包裹
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>优势:
- 无需检查 状态
isLoading - 与Suspense边界集成
- 组件代码更简洁
- 加载体验一致
useSuspenseQuery Patterns
useSuspenseQuery 模式
Basic Usage
基础用法
typescript
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => userApi.get(userId),
});
// data is never undefined - guaranteed by Suspense
return <div>{data.name}</div>;typescript
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => userApi.get(userId),
});
// 借助Suspense,data永远不会为undefined
return <div>{data.name}</div>;With Parameters
带参数用法
typescript
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useSuspenseQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => postsApi.getByUser(userId),
});
return <div>{posts.length} posts</div>;
}typescript
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useSuspenseQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => postsApi.getByUser(userId),
});
return <div>{posts.length} posts</div>;
}Dependent Queries
依赖查询
typescript
function PostDetails({ postId }: { postId: string }) {
// First query
const { data: post } = useSuspenseQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// Second query depends on first
const { data: author } = useSuspenseQuery({
queryKey: ['users', post.authorId],
queryFn: () => userApi.get(post.authorId),
});
return <div>{author.name} wrote {post.title}</div>;
}typescript
function PostDetails({ postId }: { postId: string }) {
// 第一个查询
const { data: post } = useSuspenseQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// 第二个查询依赖第一个查询的结果
const { data: author } = useSuspenseQuery({
queryKey: ['users', post.authorId],
queryFn: () => userApi.get(post.authorId),
});
return <div>{author.name} wrote {post.title}</div>;
}useQuery (Legacy Pattern)
useQuery(遗留模式)
Use only when you need loading/error states in the component:
useQuerytypescript
import { useQuery } from '@tanstack/react-query';
function Component() {
const { data, isPending, error } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
if (isPending) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{data.map(...)}</div>;
}When to use vs :
useQueryuseSuspenseQuery- Use by default (preferred)
useSuspenseQuery - Use only when you need component-level loading states
useQuery - Most cases should use + Suspense boundaries
useSuspenseQuery
仅当需要在组件内处理加载/错误状态时,才使用:
useQuerytypescript
import { useQuery } from '@tanstack/react-query';
function Component() {
const { data, isPending, error } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
if (isPending) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{data.map(...)}</div>;
}useQuery 与 useSuspenseQuery 的选择场景:
- 默认优先使用
useSuspenseQuery - 仅当需要组件级加载状态时使用
useQuery - 大多数场景应使用+ Suspense边界
useSuspenseQuery
Mutations
Mutation 操作
Basic Mutation
基础Mutation
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostButton() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: postsApi.create,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleCreate = () => {
mutation.mutate({
title: 'New Post',
content: 'Content here',
});
};
return (
<button onClick={handleCreate} disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostButton() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: postsApi.create,
onSuccess: () => {
// 失效并重新获取数据
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleCreate = () => {
mutation.mutate({
title: 'New Post',
content: 'Content here',
});
};
return (
<button onClick={handleCreate} disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}Optimistic Updates
乐观更新
typescript
const mutation = useMutation({
mutationFn: postsApi.update,
onMutate: async (updatedPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
// Snapshot previous value
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
// Optimistically update
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
// Return context with snapshot
return { previousPost };
},
onError: (err, updatedPost, context) => {
// Rollback on error
queryClient.setQueryData(
['posts', updatedPost.id],
context.previousPost
);
},
onSettled: (data, error, variables) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
},
});typescript
const mutation = useMutation({
mutationFn: postsApi.update,
onMutate: async (updatedPost) => {
// 取消正在进行的重新获取请求
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
// 快照之前的值
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
// 执行乐观更新
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
// 返回包含快照的上下文
return { previousPost };
},
onError: (err, updatedPost, context) => {
// 错误时回滚
queryClient.setQueryData(
['posts', updatedPost.id],
context.previousPost
);
},
onSettled: (data, error, variables) => {
// Mutation完成后重新获取数据
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
},
});Cache Management
缓存管理
Invalidation
缓存失效
typescript
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// Invalidate all posts queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate specific post
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
// Invalidate all queries
queryClient.invalidateQueries();typescript
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// 使所有posts相关查询失效
queryClient.invalidateQueries({ queryKey: ['posts'] });
// 使特定post查询失效
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
// 使所有查询失效
queryClient.invalidateQueries();Manual Updates
手动更新缓存
typescript
// Update cache directly
queryClient.setQueryData(['posts', postId], newPost);
// Update with function
queryClient.setQueryData(['posts'], (oldPosts) => [
...oldPosts,
newPost,
]);typescript
// 直接更新缓存
queryClient.setQueryData(['posts', postId], newPost);
// 通过函数更新缓存
queryClient.setQueryData(['posts'], (oldPosts) => [
...oldPosts,
newPost,
]);Prefetching
预获取数据
typescript
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// In a component
const prefetchPost = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
};
<Link
to={`/posts/${post.id}`}
onMouseEnter={() => prefetchPost(post.id)}
>
{post.title}
</Link>typescript
// 预获取数据
await queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// 在组件中使用
const prefetchPost = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
};
<Link
to={`/posts/${post.id}`}
onMouseEnter={() => prefetchPost(post.id)}
>
{post.title}
</Link>API Service Pattern
API服务模式
Centralized API Service
集中式API服务
typescript
// features/posts/api/postsApi.ts
import { apiClient } from '@/lib/apiClient';
import type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';
export const postsApi = {
getAll: async (): Promise<Post[]> => {
const response = await apiClient.get('/posts');
return response.data;
},
get: async (id: string): Promise<Post> => {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
},
create: async (data: CreatePostDto): Promise<Post> => {
const response = await apiClient.post('/posts', data);
return response.data;
},
update: async (id: string, data: UpdatePostDto): Promise<Post> => {
const response = await apiClient.put(`/posts/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/posts/${id}`);
},
getByUser: async (userId: string): Promise<Post[]> => {
const response = await apiClient.get(`/users/${userId}/posts`);
return response.data;
},
};typescript
// features/posts/api/postsApi.ts
import { apiClient } from '@/lib/apiClient';
import type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';
export const postsApi = {
getAll: async (): Promise<Post[]> => {
const response = await apiClient.get('/posts');
return response.data;
},
get: async (id: string): Promise<Post> => {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
},
create: async (data: CreatePostDto): Promise<Post> => {
const response = await apiClient.post('/posts', data);
return response.data;
},
update: async (id: string, data: UpdatePostDto): Promise<Post> => {
const response = await apiClient.put(`/posts/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/posts/${id}`);
},
getByUser: async (userId: string): Promise<Post[]> => {
const response = await apiClient.get(`/users/${userId}/posts`);
return response.data;
},
};Usage in Components
在组件中使用
typescript
import { postsApi } from '~/features/posts/api/postsApi';
// In query
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// In mutation
const mutation = useMutation({
mutationFn: postsApi.create,
});typescript
import { postsApi } from '~/features/posts/api/postsApi';
// 在查询中使用
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// 在Mutation中使用
const mutation = useMutation({
mutationFn: postsApi.create,
});Query Keys
查询键(Query Keys)
Key Structure
键结构
typescript
// List queries
['posts'] // All posts
['posts', { status: 'published' }] // Filtered posts
// Detail queries
['posts', postId] // Single post
['posts', postId, 'comments'] // Post comments
// Nested resources
['users', userId, 'posts'] // User's posts
['users', userId, 'posts', postId] // Specific user posttypescript
// 列表查询
['posts'] // 所有文章
['posts', { status: 'published' }] // 已发布的文章
// 详情查询
['posts', postId] // 单篇文章
['posts', postId, 'comments'] // 文章评论
// 嵌套资源
['users', userId, 'posts'] // 用户的文章
['users', userId, 'posts', postId] // 用户的特定文章Key Factories
查询键工厂
typescript
// features/posts/api/postKeys.ts
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: string) => [...postKeys.lists(), { filters }] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
comments: (id: string) => [...postKeys.detail(id), 'comments'] as const,
};
// Usage
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(postId),
queryFn: () => postsApi.get(postId),
});
// Invalidate all post lists
queryClient.invalidateQueries({ queryKey: postKeys.lists() });typescript
// features/posts/api/postKeys.ts
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: string) => [...postKeys.lists(), { filters }] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
comments: (id: string) => [...postKeys.detail(id), 'comments'] as const,
};
// 使用示例
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(postId),
queryFn: () => postsApi.get(postId),
});
// 使所有文章列表查询失效
queryClient.invalidateQueries({ queryKey: postKeys.lists() });Error Handling
错误处理
With Error Boundaries
结合错误边界
typescript
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
// In component
function DataComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
// Errors automatically caught by ErrorBoundary
});
return <div>{data}</div>;
}typescript
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
// 在组件中
function DataComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
// 错误会被ErrorBoundary自动捕获
});
return <div>{data}</div>;
}Retry and Cache Configuration
重试与缓存配置
typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
retry: 3, // Retry 3 times
retryDelay: 1000, // Wait 1s between retries
gcTime: 5 * 60 * 1000, // Garbage collection time: 5 minutes (v5: was 'cacheTime')
});typescript
const { data } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
retry: 3, // 重试3次
retryDelay: 1000, // 重试间隔1秒
gcTime: 5 * 60 * 1000, // 垃圾回收时间:5分钟(v5版本中替代原`cacheTime`)
});Best Practices
最佳实践
1. Use Suspense by Default
1. 默认使用Suspense
typescript
// ✅ Good: useSuspenseQuery + Suspense
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
function DataComponent() {
const { data } = useSuspenseQuery({...});
return <div>{data}</div>;
}
// ❌ Avoid: useQuery with manual loading
function DataComponent() {
const { data, isPending } = useQuery({...});
if (isPending) return <Spinner />;
return <div>{data}</div>;
}typescript
// ✅ 推荐:useSuspenseQuery + Suspense
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
function DataComponent() {
const { data } = useSuspenseQuery({...});
return <div>{data}</div>;
}
// ❌ 不推荐:使用useQuery手动处理加载状态
function DataComponent() {
const { data, isPending } = useQuery({...});
if (isPending) return <Spinner />;
return <div>{data}</div>;
}2. Consistent Query Keys
2. 保持查询键一致
typescript
// ✅ Good: Use key factories
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(id),
queryFn: () => postsApi.get(id),
});
// ❌ Avoid: Inconsistent keys
const { data } = useSuspenseQuery({
queryKey: ['post', id], // Different format
queryFn: () => postsApi.get(id),
});typescript
// ✅ 推荐:使用键工厂
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(id),
queryFn: () => postsApi.get(id),
});
// ❌ 不推荐:键格式不一致
const { data } = useSuspenseQuery({
queryKey: ['post', id], // 格式不同
queryFn: () => postsApi.get(id),
});3. Centralized API Services
3. 使用集中式API服务
typescript
// ✅ Good: API service
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// ❌ Avoid: Inline fetching
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
},
});typescript
// ✅ 推荐:使用API服务
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// ❌ 不推荐:内联请求
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
},
});Additional Resources
更多资源
For more patterns, see:
- data-fetching.md - Advanced patterns
- cache-strategies.md - Cache management
- mutation-patterns.md - Complex mutations
如需了解更多模式,请查看:
- data-fetching.md - 高级模式
- cache-strategies.md - 缓存管理
- mutation-patterns.md - 复杂Mutation操作