tanstack-query by tanstack-skills/tanstack-skills
npx skills add https://github.com/tanstack-skills/tanstack-skills --skill tanstack-queryTanStack Query(原名 React Query)管理服务器状态——即存储在服务器上需要获取、缓存、同步和更新的数据。它开箱即用地提供了自动缓存、后台重新获取、陈旧数据重新验证模式、分页、无限滚动和乐观更新功能。
包: @tanstack/react-query 开发者工具: @tanstack/react-query-devtools 当前版本: v5
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools # 可选
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 分钟
gcTime: 1000 * 60 * 5, // 5 分钟(垃圾回收)
retry: 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
查询键唯一标识缓存的数据。它们必须是可序列化的数组:
// 简单键
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// 带变量(依赖数组模式)
useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodos })
// 用于失效的分层键
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
useQuery({ queryKey: ['todos', todoId, 'comments'], queryFn: () => fetchComments(todoId) })
// 失效匹配前缀:
// queryClient.invalidateQueries({ queryKey: ['todos'] })
// ^ 使所有以 'todos' 开头的查询失效
// 查询函数接收一个 QueryFunctionContext
useQuery({
queryKey: ['todos', todoId],
queryFn: async ({ queryKey, signal, meta }) => {
const [_key, id] = queryKey
const response = await fetch(`/api/todos/${id}`, { signal })
if (!response.ok) throw new Error('Failed to fetch')
return response.json()
},
})
// 使用 signal 进行自动取消
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal })
return response.json()
},
})
创建可复用、类型安全的查询配置:
import { queryOptions } from '@tanstack/react-query'
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000,
})
export const todoQueryOptions = (todoId: string) =>
queryOptions({
queryKey: ['todos', todoId],
queryFn: () => fetchTodo(todoId),
enabled: !!todoId,
})
// 用法
const { data } = useQuery(todosQueryOptions)
const { data } = useSuspenseQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todosQueryOptions)
import { useQuery } from '@tanstack/react-query'
function Todos() {
const {
data,
error,
isLoading, // 首次加载,尚无数据
isFetching, // 任何进行中的获取(包括后台)
isError,
isSuccess,
isPending, // 尚无数据(大多数情况下与 isLoading 相同)
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'fetching' | 'paused' | 'idle'
refetch,
isStale,
isPlaceholderData,
dataUpdatedAt,
errorUpdatedAt,
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isLoading) return <Spinner />
if (isError) return <Error message={error.message} />
return <TodoList todos={data} />
}
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 新鲜度
staleTime: 5000, // 数据保持新鲜的毫秒数(默认:0)
gcTime: 300000, // 未使用数据保留在缓存中的毫秒数(默认:5 分钟)
// 重新获取
refetchInterval: 10000, // 每 10 秒轮询一次
refetchIntervalInBackground: false, // 标签页隐藏时不轮询
refetchOnMount: true, // 组件挂载时若数据陈旧则重新获取
refetchOnWindowFocus: true, // 窗口获得焦点时若数据陈旧则重新获取
refetchOnReconnect: true, // 网络重新连接时重新获取
// 重试
retry: 3, // 重试次数(或函数)
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// 条件性
enabled: !!userId, // 仅在为真值时运行
// 初始/占位数据
initialData: () => cachedData,
initialDataUpdatedAt: Date.now() - 10000,
placeholderData: (previousData) => previousData, // keepPreviousData 模式
placeholderData: initialTodos,
// 转换
select: (data) => data.filter(todo => !todo.done),
// 结构共享(默认:true)
structuralSharing: true,
// 网络模式
networkMode: 'online', // 'online' | 'always' | 'offlineFirst'
// 元数据(可在查询函数上下文中访问)
meta: { purpose: 'user-facing' },
})
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo: { title: string }) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).then(res => res.json())
},
// 生命周期回调
onMutate: async (variables) => {
// 在 mutationFn 之前调用
// 适用于乐观更新
return { previousTodos } // 用于 onError 的上下文
},
onSuccess: (data, variables, context) => {
// 使相关查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onError: (error, variables, context) => {
// 回滚乐观更新
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: (data, error, variables, context) => {
// 总是运行(成功或错误)
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button
onClick={() => mutation.mutate({ title: 'New Todo' })}
disabled={mutation.isPending}
>
{mutation.isPending ? '添加中...' : '添加待办事项'}
</button>
)
}
const {
mutate, // 触发后不管
mutateAsync, // 返回 Promise
isPending, // 变更进行中
isError,
isSuccess,
isIdle, // 尚未触发
data, // 成功响应
error, // 错误对象
reset, // 重置状态为 idle
variables, // 传递给 mutate 的变量
status, // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 1. 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// 2. 快照先前值
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
// 3. 乐观更新
queryClient.setQueryData(['todos', newTodo.id], newTodo)
// 4. 返回用于回滚的上下文
return { previousTodo }
},
onError: (err, newTodo, context) => {
// 出错时回滚
queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
},
onSettled: () => {
// 总是重新获取以与服务器同步
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
const queryClient = useQueryClient()
// 使所有查询失效
queryClient.invalidateQueries()
// 按前缀失效
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 精确匹配失效
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true })
// 使用谓词失效
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.status === 'done',
})
// 失效并立即重新获取
queryClient.refetchQueries({ queryKey: ['todos'] })
// 完全从缓存中移除
queryClient.removeQueries({ queryKey: ['todos', 1] })
// 重置到初始状态
queryClient.resetQueries({ queryKey: ['todos'] })
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteList() {
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/projects?cursor=${pageParam}`)
return res.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
return lastPage.nextCursor ?? undefined // undefined = 没有更多页面
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
return firstPage.prevCursor ?? undefined
},
maxPages: 3, // 在缓存中最多保留 3 页(为了性能)
})
return (
<div>
{data.pages.map((page) =>
page.items.map((item) => <Item key={item.id} item={item} />)
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : hasNextPage ? '加载更多' : '没有更多了'}
</button>
</div>
)
}
// 多个独立的查询自动并行运行
function Dashboard() {
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
// 两者同时获取
}
// 使用 useQueries 进行动态并行查询
function UserProjects({ userIds }) {
const queries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
}),
})
}
// 使用 enabled 进行顺序查询
function UserPosts({ userId }) {
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPostsByUser(userId),
enabled: !!userQuery.data, // 仅在用户数据加载后运行
})
}
function PaginatedList() {
const [page, setPage] = useState(1)
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData, // 继续显示旧数据
})
return (
<div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data.items.map(item => <Item key={item.id} item={item} />)}
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
下一页
</button>
</div>
)
}
import { useSuspenseQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'
// 组件将暂停直到数据加载完成
function TodoList() {
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 此处 data 保证已定义
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}
// 用 Suspense 边界包裹
function App() {
return (
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<TodoList />
</Suspense>
</ErrorBoundary>
)
}
// 多个 suspense 查询(并行获取)
function Dashboard() {
const [{ data: users }, { data: projects }] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
}
const queryClient = useQueryClient()
// 悬停时预取
function TodoLink({ todoId }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
staleTime: 5000, // 仅在数据超过 5 秒未更新时预取
})
}
return (
<Link to={`/todos/${todoId}`} onMouseEnter={prefetch}>
待办事项 {todoId}
</Link>
)
}
// 在路由加载器中预取(TanStack Router 集成)
export const Route = createFileRoute('/todos/$todoId')({
loader: ({ context: { queryClient }, params: { todoId } }) =>
queryClient.ensureQueryData(todoQueryOptions(todoId)),
})
// 预取无限查询
queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
pages: 3, // 预取前 3 页
})
// 服务器组件或加载器
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Page({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Todos />
</HydrationBoundary>
)
}
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { makeQueryClient } from './query-client'
export default async function Page() {
const queryClient = makeQueryClient()
// 在服务器上预取
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList />
</HydrationBoundary>
)
}
const queryClient = useQueryClient()
// 获取缓存数据
queryClient.getQueryData(['todos'])
// 设置缓存数据
queryClient.setQueryData(['todos'], updatedTodos)
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// 获取查询状态
queryClient.getQueryState(['todos'])
// 检查是否正在获取
queryClient.isFetching({ queryKey: ['todos'] })
queryClient.isMutating()
// 取消查询
queryClient.cancelQueries({ queryKey: ['todos'] })
// 失效(标记为陈旧,重新获取活跃查询)
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 重新获取(即使数据新鲜也强制重新获取)
queryClient.refetchQueries({ queryKey: ['todos'] })
// 从缓存中移除
queryClient.removeQueries({ queryKey: ['todos'] })
// 重置到初始状态
queryClient.resetQueries({ queryKey: ['todos'] })
// 清除整个缓存
queryClient.clear()
// 预取
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.ensureQueryData({ queryKey: ['todos'], queryFn: fetchTodos })
// 获取/设置默认值
queryClient.setQueryDefaults(['todos'], { staleTime: 10000 })
queryClient.getQueryDefaults(['todos'])
queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo })
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false, // 测试中不重试
gcTime: Infinity, // 防止测试期间垃圾回收
},
},
})
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
test('fetches todos', async () => {
const { result } = renderHook(() => useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
}), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(expectedTodos)
})
// 使用 setQueryData 模拟组件测试
test('renders todos', () => {
const queryClient = new QueryClient()
queryClient.setQueryData(['todos'], mockTodos)
render(
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
)
expect(screen.getByText('Todo 1')).toBeInTheDocument()
})
interface Todo {
id: number
title: string
completed: boolean
}
// 类型从 queryFn 返回类型推断
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos')
return res.json()
},
})
// data: Todo[] | undefined
// 使用 select
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data): string[] => data.map(t => t.title),
})
// data: string[] | undefined
// 默认错误类型是 Error
const { error } = useQuery<Todo[], AxiosError>({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 或全局注册
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError
}
}
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
export const todosOptions = queryOptions({
queryKey: ['todos'] as const,
queryFn: fetchTodos,
staleTime: 5000,
})
export const todoOptions = (id: string) =>
queryOptions({
queryKey: ['todos', id] as const,
queryFn: () => fetchTodo(id),
enabled: !!id,
})
// 各处都有完整的类型推断
const { data } = useQuery(todosOptions)
const { data } = useSuspenseQuery(todoOptions('123'))
await queryClient.ensureQueryData(todosOptions)
queryClient.invalidateQueries({ queryKey: todosOptions.queryKey })
// 全局禁用
const queryClient = new QueryClient({
defaultOptions: {
queries: { refetchOnWindowFocus: false },
},
})
// 自定义焦点管理器
import { focusManager } from '@tanstack/react-query'
// 用于 React Native
focusManager.setEventListener((handleFocus) => {
const subscription = AppState.addEventListener('change', (state) => {
handleFocus(state === 'active')
})
return () => subscription.remove()
})
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 'online' (默认): 仅在在线时获取
// 'always': 总是获取(适用于本地优先)
// 'offlineFirst': 尝试获取,如果离线则使用缓存
networkMode: 'offlineFirst',
})
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
// signal 是 AbortSignal - 在卸载或键更改时自动取消
const res = await fetch('/api/todos', { signal })
return res.json()
},
})
// 手动取消
queryClient.cancelQueries({ queryKey: ['todos'] })
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 小时
})
queryOptions 辅助函数 创建类型安全、可复用的查询配置staleTime - 0 表示挂载时总是重新获取(默认),对动态性较低的数据可增加此值placeholderData(而非 initialData)以在分页期间保留前一页的数据useSuspenseQuery 以获得更简洁的组件代码enabled 处理依赖查询,而不是条件性地调用钩子onMutate 中取消查询 以防止在乐观更新前出现竞态条件ensureQueryData 而不是 prefetchQuery 以实现即时访问retry: false 以避免超时问题select 处理派生数据,而不是在组件中转换gcTime: Infinity 以防止在断言期间缓存被清理placeholderData 时使用了 initialData(initialData 被视为"新鲜"数据)initialPageParam(v5 中必需)staleTime 设置得高于 gcTime(数据在"新鲜"时就被垃圾回收)QueryClientProvider 包裹QueryClient 实例(共享状态)invalidateQueries每周安装量
464
仓库
GitHub 星标数
5
首次出现
2026年2月21日
安全审计
安装于
codex452
opencode450
cursor450
gemini-cli449
github-copilot449
amp448
TanStack Query (formerly React Query) manages server state - data that lives on the server and needs to be fetched, cached, synchronized, and updated. It provides automatic caching, background refetching, stale-while-revalidate patterns, pagination, infinite scrolling, and optimistic updates out of the box.
Package: @tanstack/react-query Devtools: @tanstack/react-query-devtools Current Version: v5
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools # Optional
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes (garbage collection)
retry: 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Query keys uniquely identify cached data. They must be serializable arrays:
// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// With variables (dependency array pattern)
useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodos })
// Hierarchical keys for invalidation
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
useQuery({ queryKey: ['todos', todoId, 'comments'], queryFn: () => fetchComments(todoId) })
// Invalidation matches prefixes:
// queryClient.invalidateQueries({ queryKey: ['todos'] })
// ^ Invalidates ALL queries starting with 'todos'
// Query function receives a QueryFunctionContext
useQuery({
queryKey: ['todos', todoId],
queryFn: async ({ queryKey, signal, meta }) => {
const [_key, id] = queryKey
const response = await fetch(`/api/todos/${id}`, { signal })
if (!response.ok) throw new Error('Failed to fetch')
return response.json()
},
})
// Using the signal for automatic cancellation
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal })
return response.json()
},
})
Create reusable, type-safe query configurations:
import { queryOptions } from '@tanstack/react-query'
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000,
})
export const todoQueryOptions = (todoId: string) =>
queryOptions({
queryKey: ['todos', todoId],
queryFn: () => fetchTodo(todoId),
enabled: !!todoId,
})
// Usage
const { data } = useQuery(todosQueryOptions)
const { data } = useSuspenseQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todosQueryOptions)
import { useQuery } from '@tanstack/react-query'
function Todos() {
const {
data,
error,
isLoading, // First load, no data yet
isFetching, // Any fetch in progress (including background)
isError,
isSuccess,
isPending, // No data yet (same as isLoading in most cases)
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'fetching' | 'paused' | 'idle'
refetch,
isStale,
isPlaceholderData,
dataUpdatedAt,
errorUpdatedAt,
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isLoading) return <Spinner />
if (isError) return <Error message={error.message} />
return <TodoList todos={data} />
}
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// Freshness
staleTime: 5000, // ms data stays fresh (default: 0)
gcTime: 300000, // ms unused data stays in cache (default: 5 min)
// Refetching
refetchInterval: 10000, // Poll every 10s
refetchIntervalInBackground: false, // Don't poll when tab hidden
refetchOnMount: true, // Refetch on component mount if stale
refetchOnWindowFocus: true, // Refetch on window focus if stale
refetchOnReconnect: true, // Refetch on network reconnect
// Retry
retry: 3, // Number of retries (or function)
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// Conditional
enabled: !!userId, // Only run when truthy
// Initial/placeholder data
initialData: () => cachedData,
initialDataUpdatedAt: Date.now() - 10000,
placeholderData: (previousData) => previousData, // keepPreviousData pattern
placeholderData: initialTodos,
// Transform
select: (data) => data.filter(todo => !todo.done),
// Structural sharing (default: true)
structuralSharing: true,
// Network mode
networkMode: 'online', // 'online' | 'always' | 'offlineFirst'
// Meta (accessible in query function context)
meta: { purpose: 'user-facing' },
})
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo: { title: string }) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).then(res => res.json())
},
// Lifecycle callbacks
onMutate: async (variables) => {
// Called before mutationFn
// Good for optimistic updates
return { previousTodos } // context for onError
},
onSuccess: (data, variables, context) => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onError: (error, variables, context) => {
// Rollback optimistic updates
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: (data, error, variables, context) => {
// Always runs (success or error)
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button
onClick={() => mutation.mutate({ title: 'New Todo' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
)
}
const {
mutate, // Fire-and-forget
mutateAsync, // Returns promise
isPending, // Mutation in progress
isError,
isSuccess,
isIdle, // Not yet fired
data, // Success response
error, // Error object
reset, // Reset state to idle
variables, // Variables passed to mutate
status, // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 1. Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// 2. Snapshot previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
// 3. Optimistically update
queryClient.setQueryData(['todos', newTodo.id], newTodo)
// 4. Return context for rollback
return { previousTodo }
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
},
onSettled: () => {
// Always refetch to sync with server
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
const queryClient = useQueryClient()
// Invalidate all queries
queryClient.invalidateQueries()
// Invalidate by prefix
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Invalidate exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true })
// Invalidate with predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.status === 'done',
})
// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'] })
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] })
// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteList() {
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/projects?cursor=${pageParam}`)
return res.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
return lastPage.nextCursor ?? undefined // undefined = no more pages
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
return firstPage.prevCursor ?? undefined
},
maxPages: 3, // Keep max 3 pages in cache (for performance)
})
return (
<div>
{data.pages.map((page) =>
page.items.map((item) => <Item key={item.id} item={item} />)
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
</div>
)
}
// Multiple independent queries run in parallel automatically
function Dashboard() {
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
// Both fetch simultaneously
}
// Dynamic parallel queries with useQueries
function UserProjects({ userIds }) {
const queries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
}),
})
}
// Sequential queries using enabled
function UserPosts({ userId }) {
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPostsByUser(userId),
enabled: !!userQuery.data, // Only run when user is loaded
})
}
function PaginatedList() {
const [page, setPage] = useState(1)
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData, // Keep showing old data
})
return (
<div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data.items.map(item => <Item key={item.id} item={item} />)}
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
Next
</button>
</div>
)
}
import { useSuspenseQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'
// Component will suspend until data is loaded
function TodoList() {
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// data is guaranteed to be defined here
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}
// Wrap with Suspense boundary
function App() {
return (
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<TodoList />
</Suspense>
</ErrorBoundary>
)
}
// Multiple suspense queries (fetch in parallel)
function Dashboard() {
const [{ data: users }, { data: projects }] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
}
const queryClient = useQueryClient()
// Prefetch on hover
function TodoLink({ todoId }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
staleTime: 5000, // Only prefetch if data older than 5s
})
}
return (
<Link to={`/todos/${todoId}`} onMouseEnter={prefetch}>
Todo {todoId}
</Link>
)
}
// Prefetch in route loader (TanStack Router integration)
export const Route = createFileRoute('/todos/$todoId')({
loader: ({ context: { queryClient }, params: { todoId } }) =>
queryClient.ensureQueryData(todoQueryOptions(todoId)),
})
// Prefetch infinite queries
queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
pages: 3, // Prefetch first 3 pages
})
// Server component or loader
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Page({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Todos />
</HydrationBoundary>
)
}
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { makeQueryClient } from './query-client'
export default async function Page() {
const queryClient = makeQueryClient()
// Prefetch on server
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList />
</HydrationBoundary>
)
}
const queryClient = useQueryClient()
// Get cached data
queryClient.getQueryData(['todos'])
// Set cached data
queryClient.setQueryData(['todos'], updatedTodos)
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// Get query state
queryClient.getQueryState(['todos'])
// Check if fetching
queryClient.isFetching({ queryKey: ['todos'] })
queryClient.isMutating()
// Cancel queries
queryClient.cancelQueries({ queryKey: ['todos'] })
// Invalidate (marks stale, refetches active)
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Refetch (force refetch even if fresh)
queryClient.refetchQueries({ queryKey: ['todos'] })
// Remove from cache
queryClient.removeQueries({ queryKey: ['todos'] })
// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })
// Clear entire cache
queryClient.clear()
// Prefetch
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.ensureQueryData({ queryKey: ['todos'], queryFn: fetchTodos })
// Get/set defaults
queryClient.setQueryDefaults(['todos'], { staleTime: 10000 })
queryClient.getQueryDefaults(['todos'])
queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo })
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false, // Don't retry in tests
gcTime: Infinity, // Prevent garbage collection during tests
},
},
})
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
test('fetches todos', async () => {
const { result } = renderHook(() => useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
}), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(expectedTodos)
})
// Mock with setQueryData for component tests
test('renders todos', () => {
const queryClient = new QueryClient()
queryClient.setQueryData(['todos'], mockTodos)
render(
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
)
expect(screen.getByText('Todo 1')).toBeInTheDocument()
})
interface Todo {
id: number
title: string
completed: boolean
}
// Type is inferred from queryFn return type
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos')
return res.json()
},
})
// data: Todo[] | undefined
// With select
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data): string[] => data.map(t => t.title),
})
// data: string[] | undefined
// Default error type is Error
const { error } = useQuery<Todo[], AxiosError>({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// Or register globally
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError
}
}
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
export const todosOptions = queryOptions({
queryKey: ['todos'] as const,
queryFn: fetchTodos,
staleTime: 5000,
})
export const todoOptions = (id: string) =>
queryOptions({
queryKey: ['todos', id] as const,
queryFn: () => fetchTodo(id),
enabled: !!id,
})
// Full type inference everywhere
const { data } = useQuery(todosOptions)
const { data } = useSuspenseQuery(todoOptions('123'))
await queryClient.ensureQueryData(todosOptions)
queryClient.invalidateQueries({ queryKey: todosOptions.queryKey })
// Disable globally
const queryClient = new QueryClient({
defaultOptions: {
queries: { refetchOnWindowFocus: false },
},
})
// Custom focus manager
import { focusManager } from '@tanstack/react-query'
// For React Native
focusManager.setEventListener((handleFocus) => {
const subscription = AppState.addEventListener('change', (state) => {
handleFocus(state === 'active')
})
return () => subscription.remove()
})
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 'online' (default): only fetch when online
// 'always': always fetch (useful for local-first)
// 'offlineFirst': try fetch, use cache if offline
networkMode: 'offlineFirst',
})
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
// signal is AbortSignal - automatically cancelled on unmount or key change
const res = await fetch('/api/todos', { signal })
return res.json()
},
})
// Manual cancellation
queryClient.cancelQueries({ queryKey: ['todos'] })
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
})
queryOptions helper for type-safe, reusable query configurationsstaleTime - 0 means always refetch on mount (default), increase for less dynamic dataplaceholderData (not initialData) for keeping previous page data during paginationuseSuspenseQuery when using Suspense boundaries for cleaner component codeenabled for dependent queries, not conditional hook callsonMutate before optimistic updates to prevent race conditionsinitialData when you mean placeholderData (initialData counts as "fresh" data)initialPageParam for infinite queries (required in v5)staleTime higher than gcTime (data gets garbage collected while "fresh")QueryClientProviderQueryClient instance across tests (shared state)invalidateQueries in mutation callbacks when order mattersWeekly Installs
464
Repository
GitHub Stars
5
First Seen
Feb 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex452
opencode450
cursor450
gemini-cli449
github-copilot449
amp448
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装
ensureQueryData in route loaders instead of prefetchQuery for immediate accessretry: false in tests to avoid timeout issuesselect for derived data instead of transforming in the componentgcTime: Infinity in tests to prevent cache cleanup during assertions