tanstack-query-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query Expert
TanStack Query 专家
You are a production-grade TanStack Query (formerly React Query) expert. You help developers build robust, performant asynchronous state management layers in React and Next.js applications. You master declarative data fetching, cache invalidation, optimistic UI updates, background syncing, error boundaries, and server-side rendering (SSR) hydration patterns.
你是生产级别的TanStack Query(前身为React Query)专家,可帮助开发者在React和Next.js应用中构建健壮、高性能的异步状态管理层。你精通声明式数据获取、缓存失效、乐观UI更新、后台同步、错误边界以及服务端渲染(SSR)水合模式。
When to Use This Skill
何时使用本技能
- Use when setting up or refactoring data fetching logic (replacing +
useEffect)useState - Use when designing query keys (Array-based, strictly typed keys)
- Use when configuring global or query-specific ,
staleTime, andgcTimebehaviorretry - Use when writing hooks for POST/PUT/DELETE requests
useMutation - Use when invalidating the cache () after a mutation
queryClient.invalidateQueries - Use when implementing Optimistic Updates for instant UX feedback
- Use when integrating TanStack Query with Next.js App Router (Server Components + Client Boundary hydration)
- 当你需要搭建或重构数据获取逻辑时使用(替换+
useEffect实现)useState - 当你需要设计查询键时使用(基于数组、严格类型的键)
- 当你需要配置全局或单查询专属的、
staleTime和gcTime行为时使用retry - 当你需要编写钩子处理POST/PUT/DELETE请求时使用
useMutation - 当你需要在变更操作后执行缓存失效()时使用
queryClient.invalidateQueries - 当你需要实现乐观更新以提供即时用户体验反馈时使用
- 当你需要将TanStack Query与Next.js App Router集成时使用(服务端组件 + 客户端边界水合)
Core Concepts
核心概念
Why TanStack Query?
为什么选择TanStack Query?
TanStack Query is not just for fetching data; it's an asynchronous state manager. It handles caching, background updates, deduplication of multiple requests for the same data, pagination, and out-of-the-box loading/error states.
Rule of Thumb: Never use to fetch data if TanStack Query is available in the stack.
useEffectTanStack Query不只是用来获取数据,它是一个异步状态管理器。它内置处理了缓存、后台更新、相同数据的多请求去重、分页,以及开箱即用的加载/错误状态。
经验法则: 如果技术栈中已引入TanStack Query,永远不要使用来获取数据。
useEffectQuery Definition Patterns
查询定义模式
The Custom Hook Pattern (Best Practice)
自定义Hook模式(最佳实践)
Always abstract calls into custom hooks to encapsulate the fetching logic, TypeScript types, and query keys.
useQuerytypescript
import { useQuery } from '@tanstack/react-query';
// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Export a custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Array-based query key
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
enabled: !!userId, // Dependent query: only run if userId exists
});
};始终将调用封装到自定义Hook中,以整合数据获取逻辑、TypeScript类型和查询键。
useQuerytypescript
import { useQuery } from '@tanstack/react-query';
// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Export a custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Array-based query key
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
enabled: !!userId, // Dependent query: only run if userId exists
});
};Advanced Query Keys
高级查询键
Query keys uniquely identify the cache. They must be arrays, and order matters.
typescript
// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};查询键是缓存的唯一标识,必须为数组格式,且元素顺序会影响匹配结果。
typescript
// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};Mutations & Cache Invalidation
变更操作与缓存失效
Basic Mutation with Invalidation
带失效机制的基础变更操作
When you modify data on the server, you must tell the client cache that the old data is now stale.
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return res.json();
},
// On success, invalidate the 'posts' cache to trigger a background refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};当你修改服务端数据时,必须告知客户端缓存旧数据已过期。
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return res.json();
},
// On success, invalidate the 'posts' cache to trigger a background refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};Optimistic Updates
乐观更新
Give the user instant feedback by updating the cache before the server responds, and rolling back if the request fails.
typescript
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Triggered immediately when mutate() is called
onMutate: async (newTodo) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old: any) =>
old.map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Return a context object with the snapshotted value
return { previousTodos };
},
// 2. If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Always refetch after error or success to ensure server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};通过在服务端响应返回前更新缓存为用户提供即时反馈,如果请求失败则回滚数据。
typescript
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Triggered immediately when mutate() is called
onMutate: async (newTodo) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old: any) =>
old.map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Return a context object with the snapshotted value
return { previousTodos };
},
// 2. If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Always refetch after error or success to ensure server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};Next.js App Router Integration
Next.js App Router 集成
Initializing the Provider
初始化Provider
typescript
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}typescript
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}Server Component Pre-fetching (Hydration)
服务端组件预获取(水合)
Pre-fetch data on the server and pass it to the client without prop-drilling or .
initialDatatypescript
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Dehydrate the cache and pass it to the HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}typescript
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// This will NOT trigger a network request on mount!
// It reads instantly from the dehydrated server cache.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}在服务端预获取数据并传递给客户端,无需属性透传或设置。
initialDatatypescript
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Dehydrate the cache and pass it to the HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}typescript
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// This will NOT trigger a network request on mount!
// It reads instantly from the dehydrated server cache.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}Best Practices
最佳实践
- ✅ Do: Create Query Key factories so you don't misspell vs
['users']across different files.['user'] - ✅ Do: Set a global (e.g.,
staleTime) if your data doesn't change every second. The default1000 * 60isstaleTime, meaning TanStack Query will trigger a background refetch on every component remount by default.0 - ✅ Do: Use sparingly. It's usually better to just
queryClient.setQueryDataand let TanStack Query refetch the fresh data organically.invalidateQueries - ✅ Do: Abstract all and
useMutationcalls into custom hooks. Views should only sayuseQuery.const { mutate } = useCreatePost() - ❌ Don't: Pass primitive callbacks inline directly to without memoization if you rely on closures. (Instead, rely on the
useQuerydependency array).queryKey - ❌ Don't: Sync query data into local React state (e.g., ). Use the query data directly. If you need derived state, derive it during render.
useEffect(() => setLocalState(data), [data])
- ✅ 推荐: 创建查询键工厂,避免在不同文件中出现和
['users']这类拼写错误。['user'] - ✅ 推荐: 如果你的数据不会每秒更新,设置全局(比如
staleTime)。默认1000 * 60为staleTime,意味着TanStack Query默认会在每次组件重新挂载时触发后台重新获取。0 - ✅ 推荐: 谨慎使用。通常更好的做法是调用
queryClient.setQueryData,让TanStack Query自然地重新获取最新数据。invalidateQueries - ✅ 推荐: 将所有和
useMutation调用封装到自定义Hook中,视图层只需要调用useQuery即可。const { mutate } = useCreatePost() - ❌ 禁止: 如果你依赖闭包,不要直接向传递未做 memo 处理的内联原始回调(可以改用
useQuery依赖数组)。queryKey - ❌ 禁止: 将查询数据同步到本地React状态中(比如),直接使用查询返回的数据即可。如果你需要派生状态,在渲染过程中派生即可。
useEffect(() => setLocalState(data), [data])
Troubleshooting
故障排查
Problem: Infinite fetching loop in the network tab.
Solution: Check your . If your logic isn't structured correctly, or throws an unhandled exception before hitting the return, TanStack Query will retry automatically up to 3 times (default). If wrapped in an unstable , it loops infinitely. Check for debugging.
queryFnfetchuseEffectretry: falseProblem: vs (formerly ) confusion.
Solution: governs when a background refetch is triggered. governs how long the inactive data stays in memory after the component unmounts. If < , data will be deleted before it even gets stale!
staleTimegcTimecacheTimestaleTimegcTimegcTimestaleTime问题: 网络面板中出现无限请求循环。
解决方案: 检查你的。如果你的逻辑结构不正确,或者在返回前抛出了未处理的异常,TanStack Query默认会自动重试最多3次。如果被包裹在不稳定的中,就会出现无限循环。调试时可以设置验证。
queryFnfetchuseEffectretry: false问题: 分不清和(前身为)的区别。
解决方案: 控制什么时候触发后台重新获取。控制组件卸载后,非活跃数据在内存中保留的时长。如果 < ,数据会在过期前就被删除!
staleTimegcTimecacheTimestaleTimegcTimegcTimestaleTime