tanstack-query-advanced
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query Advanced
TanStack Query 高级用法
Production patterns for TanStack Query v5 - server state management done right.
TanStack Query v5 生产级实践——正确管理服务端状态。
Overview
概述
- Infinite scroll / pagination
- Optimistic UI updates
- Prefetching for instant navigation
- Complex cache invalidation
- Dependent/parallel queries
- Mutations with rollback
- 无限滚动/分页
- 乐观UI更新
- 为即时导航预取数据
- 复杂缓存失效
- 依赖/并行查询
- 支持回滚的变更操作
Core Patterns
核心实践
1. Infinite Queries (Cursor-Based)
1. 基于游标无限查询
typescript
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Item[];
nextCursor: string | null;
}
function useInfiniteItems() {
return useInfiniteQuery({
queryKey: ['items'],
queryFn: async ({ pageParam }): Promise<Page> => {
const res = await fetch(`/api/items?cursor=${pageParam ?? ''}`);
return res.json();
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
}
// Component
function ItemList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();
return (
<>
{data?.pages.flatMap((page) => page.items.map((item) => (
<ItemCard key={item.id} item={item} />
)))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
</>
);
}typescript
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Item[];
nextCursor: string | null;
}
function useInfiniteItems() {
return useInfiniteQuery({
queryKey: ['items'],
queryFn: async ({ pageParam }): Promise<Page> => {
const res = await fetch(`/api/items?cursor=${pageParam ?? ''}`);
return res.json();
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
}
// Component
function ItemList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();
return (
<>
{data?.pages.flatMap((page) => page.items.map((item) => (
<ItemCard key={item.id} item={item} />
)))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
</>
);
}2. Optimistic Updates
2. 乐观更新
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });
// Snapshot previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);
// Optimistically update
queryClient.setQueryData(['todos', newTodo.id], newTodo);
// Return context for rollback
return { previousTodo };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
},
onSettled: (data, error, variables) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
},
});
}typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });
// Snapshot previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);
// Optimistically update
queryClient.setQueryData(['todos', newTodo.id], newTodo);
// Return context for rollback
return { previousTodo };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
},
onSettled: (data, error, variables) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
},
});
}3. Prefetching Patterns
3. 预取实践
typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
return (
<Link to={`/users/${userId}`} onMouseEnter={prefetchUser}>
View User
</Link>
);
}
// Prefetch in loader (React Router)
export const loader = (queryClient: QueryClient) => async ({ params }) => {
await queryClient.ensureQueryData({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
return null;
};typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
return (
<Link to={`/users/${userId}`} onMouseEnter={prefetchUser}>
View User
</Link>
);
}
// Prefetch in loader (React Router)
export const loader = (queryClient: QueryClient) => async ({ params }) => {
await queryClient.ensureQueryData({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
return null;
};4. Smart Cache Invalidation
4. 智能缓存失效
typescript
const queryClient = useQueryClient();
// Invalidate exact query
queryClient.invalidateQueries({ queryKey: ['todos', 1] });
// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Invalidate with predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
(query.queryKey[1] as Todo)?.status === 'done',
});
// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] });typescript
const queryClient = useQueryClient();
// Invalidate exact query
queryClient.invalidateQueries({ queryKey: ['todos', 1] });
// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Invalidate with predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
(query.queryKey[1] as Todo)?.status === 'done',
});
// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] });5. Dependent Queries
5. 依赖查询
typescript
function useUserPosts(userId: string) {
// First query
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Dependent query - only runs when user is loaded
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data, // Only fetch when user exists
});
return { user: userQuery.data, posts: postsQuery.data };
}typescript
function useUserPosts(userId: string) {
// First query
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Dependent query - only runs when user is loaded
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data, // Only fetch when user exists
});
return { user: userQuery.data, posts: postsQuery.data };
}6. Parallel Queries
6. 并行查询
typescript
import { useQueries } from '@tanstack/react-query';
function useMultipleUsers(userIds: string[]) {
return useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
})),
combine: (results) => ({
users: results.map((r) => r.data).filter(Boolean),
pending: results.some((r) => r.isPending),
error: results.find((r) => r.error)?.error,
}),
});
}typescript
import { useQueries } from '@tanstack/react-query';
function useMultipleUsers(userIds: string[]) {
return useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
})),
combine: (results) => ({
users: results.map((r) => r.data).filter(Boolean),
pending: results.some((r) => r.isPending),
error: results.find((r) => r.error)?.error,
}),
});
}7. Query Deduplication & Batching
7. 查询去重与批处理
typescript
// Configure in QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});typescript
// Configure in QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});8. Suspense Integration
8. Suspense 集成
typescript
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
// This will suspend until data is ready
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>;
}
// Wrap with Suspense
<Suspense fallback={<Skeleton />}>
<UserProfile userId="123" />
</Suspense>typescript
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
// This will suspend until data is ready
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>;
}
// Wrap with Suspense
<Suspense fallback={<Skeleton />}>
<UserProfile userId="123" />
</Suspense>9. Mutation State Tracking
9. 变更状态追踪
typescript
import { useMutationState } from '@tanstack/react-query';
function PendingTodos() {
// Track all pending todo mutations
const pendingMutations = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
return (
<>
{pendingMutations.map((todo) => (
<TodoItem key={todo.id} todo={todo} isPending />
))}
</>
);
}typescript
import { useMutationState } from '@tanstack/react-query';
function PendingTodos() {
// Track all pending todo mutations
const pendingMutations = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
return (
<>
{pendingMutations.map((todo) => (
<TodoItem key={todo.id} todo={todo} isPending />
))}
</>
);
}Configuration Best Practices
配置最佳实践
typescript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // Data fresh for 1 min
gcTime: 1000 * 60 * 5, // Cache for 5 min
refetchOnWindowFocus: true, // Refetch on tab focus
refetchOnReconnect: true, // Refetch on network reconnect
retry: 3, // Retry failed requests
},
mutations: {
retry: 1,
onError: (error) => toast.error(error.message),
},
},
});typescript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // Data fresh for 1 min
gcTime: 1000 * 60 * 5, // Cache for 5 min
refetchOnWindowFocus: true, // Refetch on tab focus
refetchOnReconnect: true, // Refetch on network reconnect
retry: 3, // Retry failed requests
},
mutations: {
retry: 1,
onError: (error) => toast.error(error.message),
},
},
});Quick Reference
快速参考
typescript
// ✅ Create typed query with queryOptions helper (v5)
const userQueryOptions = (id: string) => queryOptions({
queryKey: ['user', id] as const,
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
});
// ✅ Use the query options for consistency
const { data } = useQuery(userQueryOptions(userId));
await queryClient.prefetchQuery(userQueryOptions(userId));
await queryClient.ensureQueryData(userQueryOptions(userId));
// ✅ v5: isPending instead of isLoading (initial load only)
if (isPending) return <Skeleton />;
// ✅ v5: gcTime instead of cacheTime
gcTime: 5 * 60 * 1000,
// ✅ useSuspenseQuery for Suspense integration
const { data } = useSuspenseQuery(userQueryOptions(userId));
// ✅ Selective invalidation
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// ❌ NEVER destructure useQuery result at call site
const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object
// ❌ NEVER use string keys
useQuery({ queryKey: 'users' }); // BAD - use arrays
// ❌ NEVER store server state in Zustand
const useStore = create((set) => ({ users: [] })); // BAD - use React Querytypescript
// ✅ Create typed query with queryOptions helper (v5)
const userQueryOptions = (id: string) => queryOptions({
queryKey: ['user', id] as const,
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
});
// ✅ Use the query options for consistency
const { data } = useQuery(userQueryOptions(userId));
await queryClient.prefetchQuery(userQueryOptions(userId));
await queryClient.ensureQueryData(userQueryOptions(userId));
// ✅ v5: isPending instead of isLoading (initial load only)
if (isPending) return <Skeleton />;
// ✅ v5: gcTime instead of cacheTime
gcTime: 5 * 60 * 1000,
// ✅ useSuspenseQuery for Suspense integration
const { data } = useSuspenseQuery(userQueryOptions(userId));
// ✅ Selective invalidation
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// ❌ NEVER destructure useQuery result at call site
const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object
// ❌ NEVER use string keys
useQuery({ queryKey: 'users' }); // BAD - use arrays
// ❌ NEVER store server state in Zustand
const useStore = create((set) => ({ users: [] })); // BAD - use React QueryKey Decisions
关键决策
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Query key structure | String | Array | Array - supports hierarchy and serialization |
| Cache timing | staleTime | gcTime | Both - staleTime for freshness, gcTime for memory |
| Loading state | isLoading | isPending | isPending (v5) - isLoading includes background refetches |
| Query definition | Inline | queryOptions | queryOptions - reusable for prefetch/loader/useQuery |
| Suspense | useQuery + loading | useSuspenseQuery | useSuspenseQuery for React 18+ Suspense |
| Optimistic updates | setQueryData only | setQueryData + invalidate | Both - optimistic then reconcile |
| Parallel queries | Multiple useQuery | useQueries | useQueries - combined loading/error state |
| Infinite queries | Manual pagination | useInfiniteQuery | useInfiniteQuery - built-in cursor handling |
| 决策项 | 选项A | 选项B | 推荐方案 |
|---|---|---|---|
| 查询键结构 | 字符串 | 数组 | 数组 - 支持层级与序列化 |
| 缓存时机 | staleTime | gcTime | 两者结合 - staleTime控制新鲜度,gcTime控制内存缓存时长 |
| 加载状态 | isLoading | isPending | isPending(v5)- isLoading包含后台重新获取状态 |
| 查询定义 | 内联 | queryOptions | queryOptions - 可复用用于预取/加载器/useQuery |
| Suspense集成 | useQuery + 加载状态 | useSuspenseQuery | useSuspenseQuery - 适用于React 18+ Suspense |
| 乐观更新 | 仅setQueryData | setQueryData + 失效 | 两者结合 - 先乐观更新再同步服务端 |
| 并行查询 | 多个useQuery | useQueries | useQueries - 合并加载/错误状态 |
| 无限查询 | 手动分页 | useInfiniteQuery | useInfiniteQuery - 内置游标处理 |
Anti-Patterns (FORBIDDEN)
反模式(禁止)
typescript
// ❌ FORBIDDEN: Storing server state in Zustand/Redux
const useStore = create((set) => ({
users: [], // Server state belongs in React Query!
fetchUsers: async () => {
const users = await api.getUsers();
set({ users }); // Stale data, no background refetch
},
}));
// ❌ FORBIDDEN: String query keys
useQuery({
queryKey: 'todos', // Must be an array!
queryFn: fetchTodos,
});
// ❌ FORBIDDEN: Using deprecated cacheTime (v5)
useQuery({
queryKey: ['todos'],
cacheTime: 5 * 60 * 1000, // WRONG - use gcTime in v5
});
// ❌ FORBIDDEN: Using isLoading for initial state (v5)
// isLoading = isPending && isFetching (includes background refetch)
if (isLoading) return <Skeleton />; // WRONG - use isPending
// ❌ FORBIDDEN: Over-invalidating after mutations
useMutation({
mutationFn: updateTodo,
onSuccess: () => {
queryClient.invalidateQueries(); // Invalidates EVERYTHING!
},
});
// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates
useMutation({
onMutate: async (newTodo) => {
// Missing: await queryClient.cancelQueries(...)
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previous };
},
});
// ❌ FORBIDDEN: Not returning context from onMutate
useMutation({
onMutate: async (newTodo) => {
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// Missing: return { previous }; // Required for rollback!
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previous); // context is undefined!
},
});
// ❌ FORBIDDEN: Mutating cache data directly
queryClient.setQueryData(['todos'], (old) => {
old.push(newTodo); // WRONG - mutates existing array
return old;
});
// ✅ CORRECT: Return new array
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// ❌ FORBIDDEN: Fetching inside useEffect
useEffect(() => {
fetch('/api/users').then(setUsers); // Use React Query instead!
}, []);typescript
// ❌ FORBIDDEN: Storing server state in Zustand/Redux
const useStore = create((set) => ({
users: [], // Server state belongs in React Query!
fetchUsers: async () => {
const users = await api.getUsers();
set({ users }); // Stale data, no background refetch
},
}));
// ❌ FORBIDDEN: String query keys
useQuery({
queryKey: 'todos', // Must be an array!
queryFn: fetchTodos,
});
// ❌ FORBIDDEN: Using deprecated cacheTime (v5)
useQuery({
queryKey: ['todos'],
cacheTime: 5 * 60 * 1000, // WRONG - use gcTime in v5
});
// ❌ FORBIDDEN: Using isLoading for initial state (v5)
// isLoading = isPending && isFetching (includes background refetch)
if (isLoading) return <Skeleton />; // WRONG - use isPending
// ❌ FORBIDDEN: Over-invalidating after mutations
useMutation({
mutationFn: updateTodo,
onSuccess: () => {
queryClient.invalidateQueries(); // Invalidates EVERYTHING!
},
});
// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates
useMutation({
onMutate: async (newTodo) => {
// Missing: await queryClient.cancelQueries(...)
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previous };
},
});
// ❌ FORBIDDEN: Not returning context from onMutate
useMutation({
onMutate: async (newTodo) => {
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// Missing: return { previous }; // Required for rollback!
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previous); // context is undefined!
},
});
// ❌ FORBIDDEN: Mutating cache data directly
queryClient.setQueryData(['todos'], (old) => {
old.push(newTodo); // WRONG - mutates existing array
return old;
});
// ✅ CORRECT: Return new array
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// ❌ FORBIDDEN: Fetching inside useEffect
useEffect(() => {
fetch('/api/users').then(setUsers); // Use React Query instead!
}, []);Related Skills
相关技能
- - Client state management (use alongside React Query for server state)
zustand-patterns - - Form state with React Hook Form (integrate mutation status)
form-state-patterns - - Mock Service Worker for testing queries without network
msw-mocking - - RSC hydration with React Query
react-server-components-framework
- - 客户端状态管理(与React Query配合用于服务端状态)
zustand-patterns - - 结合React Hook Form的表单状态(集成变更状态)
form-state-patterns - - Mock Service Worker,无需网络即可测试查询
msw-mocking - - React Query与RSC水合
react-server-components-framework
Capability Details
功能细节
infinite-queries
infinite-queries
Keywords: infinite, pagination, cursor, load more, scroll, pages
Solves: Implementing cursor-based pagination with automatic page management
关键词: infinite, pagination, cursor, load more, scroll, pages
解决问题: 实现基于游标的分页,自动管理页面
optimistic-updates
optimistic-updates
Keywords: optimistic, instant, rollback, onMutate, setQueryData, cancel
Solves: Showing immediate UI feedback before server confirmation with rollback
关键词: optimistic, instant, rollback, onMutate, setQueryData, cancel
解决问题: 在服务端确认前显示即时UI反馈,并支持回滚
prefetching
prefetching
Keywords: prefetch, hover, preload, ensureQueryData, loader, navigation
Solves: Loading data before it's needed for instant navigation
关键词: prefetch, hover, preload, ensureQueryData, loader, navigation
解决问题: 在需要前加载数据,实现即时导航
cache-invalidation
cache-invalidation
Keywords: invalidate, refetch, stale, fresh, gcTime, staleTime, exact
Solves: Keeping cache in sync with server after mutations
关键词: invalidate, refetch, stale, fresh, gcTime, staleTime, exact
解决问题: 变更后保持缓存与服务端同步
suspense-integration
suspense-integration
Keywords: suspense, useSuspenseQuery, streaming, fallback, boundary
Solves: Integrating with React Suspense for declarative loading states
关键词: suspense, useSuspenseQuery, streaming, fallback, boundary
解决问题: 与React Suspense集成,实现声明式加载状态
parallel-queries
parallel-queries
Keywords: useQueries, parallel, concurrent, combine, batch
Solves: Fetching multiple independent queries with combined state
关键词: useQueries, parallel, concurrent, combine, batch
解决问题: 获取多个独立查询,并合并状态
References
参考资料
- - Cache invalidation patterns
references/cache-strategies.md - - Production query hook template
scripts/query-hooks-template.ts - - Implementation checklist
checklists/tanstack-checklist.md - - Real-world usage examples
examples/tanstack-examples.md
- - 缓存失效模式
references/cache-strategies.md - - 生产级查询Hook模板
scripts/query-hooks-template.ts - - 实现检查清单
checklists/tanstack-checklist.md - - 真实场景使用示例
examples/tanstack-examples.md