react-query-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack React Query Patterns
TanStack React Query 模式
Setup
配置
Query Client
查询客户端
tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分钟
gcTime: 5 * 60 * 1000, // 5分钟(原cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Queries
查询
Basic Query
基础查询
Fetch data with automatic caching:
tsx
'use client';
import { useQuery } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
}
export function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json() as Promise<User>;
},
});
if (isLoading) return <LoadingSkeleton />;
if (error) return <ErrorMessage error={error} />;
return <div>{data.name}</div>;
}通过自动缓存获取数据:
tsx
'use client';
import { useQuery } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
}
export function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('获取用户失败');
return response.json() as Promise<User>;
},
});
if (isLoading) return <LoadingSkeleton />;
if (error) return <ErrorMessage error={error} />;
return <div>{data.name}</div>;
}Query with Options
带选项的查询
tsx
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['posts', { page, filter }],
queryFn: () => fetchPosts(page, filter),
enabled: !!userId, // Only run if userId exists
staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
retry: 3, // Retry failed requests 3 times
refetchInterval: 30 * 1000, // Refetch every 30 seconds
refetchOnWindowFocus: true, // Refetch when window regains focus
});tsx
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['posts', { page, filter }],
queryFn: () => fetchPosts(page, filter),
enabled: !!userId, // 仅当userId存在时执行
staleTime: 5 * 60 * 1000, // 数据5分钟内保持新鲜
gcTime: 10 * 60 * 1000, // 在缓存中保留10分钟
retry: 3, // 失败请求重试3次
refetchInterval: 30 * 1000, // 每30秒重新获取一次
refetchOnWindowFocus: true, // 当窗口重新获得焦点时重新获取
});Query Keys
查询键
Structure
结构
Hierarchical query key organization:
| Pattern | Example |
|---|---|
| Simple | |
| With ID | |
| With params | |
| Nested | |
分层查询键组织:
| 模式 | 示例 |
|---|---|
| 简单 | |
| 带ID | |
| 带参数 | |
| 嵌套 | |
Best Practices
最佳实践
- Use arrays for all query keys
- Order from general to specific
- Include all variables that affect the query
- Use objects for multiple parameters
- Keep keys consistent across the app
- 所有查询键都使用数组
- 顺序从通用到具体
- 包含所有影响查询的变量
- 多个参数使用对象
- 在整个应用中保持键的一致性
Query Key Factory
查询键工厂
tsx
// Query key organization
const queryKeys = {
users: ['users'] as const,
user: (id: string) => ['users', id] as const,
userPosts: (id: string) => ['users', id, 'posts'] as const,
posts: {
all: ['posts'] as const,
lists: () => ['posts', 'list'] as const,
list: (filters: PostFilters) => ['posts', 'list', filters] as const,
detail: (id: string) => ['posts', id] as const,
},
};
// Usage
const { data } = useQuery({
queryKey: queryKeys.user(userId),
queryFn: () => fetchUser(userId),
});tsx
// 查询键组织
const queryKeys = {
users: ['users'] as const,
user: (id: string) => ['users', id] as const,
userPosts: (id: string) => ['users', id, 'posts'] as const,
posts: {
all: ['posts'] as const,
lists: () => ['posts', 'list'] as const,
list: (filters: PostFilters) => ['posts', 'list', filters] as const,
detail: (id: string) => ['posts', id] as const,
},
};
// 使用示例
const { data } = useQuery({
queryKey: queryKeys.user(userId),
queryFn: () => fetchUser(userId),
});Mutations
数据突变(Mutations)
Basic Mutation
基础突变
Modify server data:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function CreatePostForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost: NewPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!response.ok) throw new Error('Failed to create post');
return response.json();
},
onSuccess: () => {
// Invalidate and refetch posts query
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleSubmit = (data: NewPost) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
{mutation.isPending && <p>Creating post...</p>}
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
</form>
);
}修改服务端数据:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function CreatePostForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost: NewPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!response.ok) throw new Error('创建帖子失败');
return response.json();
},
onSuccess: () => {
// 使帖子查询失效并重新获取
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleSubmit = (data: NewPost) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
{/* 表单字段 */}
{mutation.isPending && <p>正在创建帖子...</p>}
{mutation.isError && <p>错误:{mutation.error.message}</p>}
{mutation.isSuccess && <p>帖子创建成功!</p>}
</form>
);
}Optimistic Updates
乐观更新
Update UI immediately before server responds:
tsx
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot current value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old: Todo[]) => {
return old.map((todo) =>
todo.id === newTodo.id ? newTodo : todo
);
});
// Return context with snapshot
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});在服务端响应前立即更新UI:
tsx
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 快照当前值
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], (old: Todo[]) => {
return old.map((todo) =>
todo.id === newTodo.id ? newTodo : todo
);
});
// 返回包含快照的上下文
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 出错时回滚
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// 出错或成功后重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});Mutation with Invalidation
带失效的突变
tsx
const mutation = useMutation({
mutationFn: deletePost,
onSuccess: (_, deletedPostId) => {
// Invalidate list query
queryClient.invalidateQueries({ queryKey: ['posts', 'list'] });
// Remove deleted post from cache
queryClient.removeQueries({ queryKey: ['posts', deletedPostId] });
// Or update cache manually
queryClient.setQueryData(['posts', 'list'], (old: Post[]) => {
return old.filter((post) => post.id !== deletedPostId);
});
},
});tsx
const mutation = useMutation({
mutationFn: deletePost,
onSuccess: (_, deletedPostId) => {
// 使列表查询失效
queryClient.invalidateQueries({ queryKey: ['posts', 'list'] });
// 从缓存中移除已删除的帖子
queryClient.removeQueries({ queryKey: ['posts', deletedPostId] });
// 或手动更新缓存
queryClient.setQueryData(['posts', 'list'], (old: Post[]) => {
return old.filter((post) => post.id !== deletedPostId);
});
},
});Infinite Queries
无限查询
Load more data as user scrolls:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
export function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/posts?page=${pageParam}`);
return response.json();
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
});
if (isLoading) return <LoadingSkeleton />;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}用户滚动时加载更多数据:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
export function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/posts?page=${pageParam}`);
return response.json();
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
});
if (isLoading) return <LoadingSkeleton />;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
)}
</div>
);
}Prefetching
预获取
Hover Prefetch
悬停预获取
Prefetch data on hover for instant navigation:
tsx
import { useQueryClient } from '@tanstack/react-query';
export function PostLink({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const prefetchPost = () => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
staleTime: 60 * 1000, // Keep for 1 minute
});
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={prefetchPost}
onTouchStart={prefetchPost}
>
View Post
</Link>
);
}在悬停时预获取数据以实现即时导航:
tsx
import { useQueryClient } from '@tanstack/react-query';
export function PostLink({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const prefetchPost = () => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
staleTime: 60 * 1000, // 保留1分钟
});
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={prefetchPost}
onTouchStart={prefetchPost}
>
查看帖子
</Link>
);
}Page Prefetch
页面预获取
tsx
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
});
// Prefetch next page
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}
}, [data, page, queryClient]);tsx
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
});
// 预获取下一页
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}
}, [data, page, queryClient]);Dependent Queries
依赖查询
Query depends on result of previous query:
tsx
// First query
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Second query depends on first
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchUserProjects(user.id),
enabled: !!user, // Only run when user exists
});查询依赖于前一个查询的结果:
tsx
// 第一个查询
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// 第二个查询依赖于第一个查询
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchUserProjects(user.id),
enabled: !!user, // 仅当user存在时执行
});Parallel Queries
并行查询
Multiple Independent Queries
多个独立查询
tsx
export function Dashboard() {
const users = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const posts = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const stats = useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
});
if (users.isLoading || posts.isLoading || stats.isLoading) {
return <LoadingSkeleton />;
}
return (
<div>
<UserList users={users.data} />
<PostList posts={posts.data} />
<Stats data={stats.data} />
</div>
);
}tsx
export function Dashboard() {
const users = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const posts = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const stats = useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
});
if (users.isLoading || posts.isLoading || stats.isLoading) {
return <LoadingSkeleton />;
}
return (
<div>
<UserList users={users.data} />
<PostList posts={posts.data} />
<Stats data={stats.data} />
</div>
);
}useQueries for Dynamic Queries
使用useQueries处理动态查询
tsx
import { useQueries } from '@tanstack/react-query';
export function MultiUserView({ userIds }: { userIds: string[] }) {
const userQueries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
});
const isLoading = userQueries.some((query) => query.isLoading);
const users = userQueries.map((query) => query.data);
if (isLoading) return <LoadingSkeleton />;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}tsx
import { useQueries } from '@tanstack/react-query';
export function MultiUserView({ userIds }: { userIds: string[] }) {
const userQueries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
});
const isLoading = userQueries.some((query) => query.isLoading);
const users = userQueries.map((query) => query.data);
if (isLoading) return <LoadingSkeleton />;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}Error Handling
错误处理
Retry Logic
重试逻辑
tsx
const { data, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: (failureCount, error) => {
// Don't retry on 404
if (error.status === 404) return false;
// Retry up to 3 times
return failureCount < 3;
},
retryDelay: (attemptIndex) => {
// Exponential backoff: 1s, 2s, 4s
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
});tsx
const { data, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: (failureCount, error) => {
// 404错误不重试
if (error.status === 404) return false;
// 最多重试3次
return failureCount < 3;
},
retryDelay: (attemptIndex) => {
// 指数退避:1秒、2秒、4秒
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
});Error Boundaries
错误边界
tsx
import { useQuery } from '@tanstack/react-query';
import { useErrorBoundary } from 'react-error-boundary';
export function CriticalData() {
const { showBoundary } = useErrorBoundary();
const { data } = useQuery({
queryKey: ['critical'],
queryFn: fetchCriticalData,
throwOnError: true, // Throw errors to Error Boundary
});
return <div>{/* render data */}</div>;
}tsx
import { useQuery } from '@tanstack/react-query';
import { useErrorBoundary } from 'react-error-boundary';
export function CriticalData() {
const { showBoundary } = useErrorBoundary();
const { data } = useQuery({
queryKey: ['critical'],
queryFn: fetchCriticalData,
throwOnError: true, // 将错误抛出到Error Boundary
});
return <div>{/* 渲染数据 */}</div>;
}Best Practices
最佳实践
Do
建议
- Use query keys as array of dependencies
- Invalidate queries after mutations
- Prefetch on hover for better UX
- Use staleTime to reduce unnecessary refetches
- Implement optimistic updates for instant feedback
- Use enabled option for dependent queries
- Keep query functions pure and reusable
- 将查询键用作依赖项数组
- 突变后使查询失效
- 悬停时预获取以提升用户体验
- 使用staleTime减少不必要的重新获取
- 实现乐观更新以获得即时反馈
- 对依赖查询使用enabled选项
- 保持查询函数纯净且可复用
Don't
不建议
- Don't use query keys as strings (use arrays)
- Don't mutate query data directly
- Don't forget to handle loading and error states
- Don't set staleTime too low (causes excessive requests)
- Don't invalidate all queries (be specific)
- Don't put business logic in queryFn (use services)
- 不要将查询键用作字符串(使用数组)
- 不要直接修改查询数据
- 不要忘记处理加载和错误状态
- 不要将staleTime设置得过低(会导致过多请求)
- 不要使所有查询失效(要具体)
- 不要在queryFn中放入业务逻辑(使用服务层)
Performance
性能
Optimization
优化
- Set appropriate staleTime (avoid unnecessary refetches)
- Use gcTime to control cache memory usage
- Implement pagination or infinite queries for large lists
- Prefetch predictable user navigation
- Use select option to subscribe to only needed data
- 设置合适的staleTime(避免不必要的重新获取)
- 使用gcTime控制缓存内存使用
- 对大型列表实现分页或无限查询
- 预获取可预测的用户导航
- 使用select选项仅订阅所需数据
Monitoring
监控
- Enable React Query Devtools in development
- Monitor network requests in DevTools
- Check query cache size regularly
- Measure query execution time
- 在开发环境中启用React Query Devtools
- 在开发者工具中监控网络请求
- 定期检查查询缓存大小
- 测量查询执行时间