tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOverview
概述
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:
Devtools:
Current Version: v5
@tanstack/react-query@tanstack/react-query-devtoolsTanStack Query(前身为React Query)用于管理服务端状态——即存储在服务端、需要被获取、缓存、同步和更新的数据。它开箱即用地提供了自动缓存、后台重新获取、stale-while-revalidate模式、分页、无限滚动和乐观更新等功能。
包:
开发者工具:
当前版本: v5
@tanstack/react-query@tanstack/react-query-devtoolsInstallation
安装
bash
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools # Optionalbash
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools # 可选Setup
配置
tsx
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>
)
}tsx
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>
)
}Core Concepts
核心概念
Query Keys
查询键(Query Keys)
Query keys uniquely identify cached data. They must be serializable arrays:
tsx
// 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'查询键用于唯一标识缓存的数据,它们必须是可序列化的数组:
tsx
// 简单键
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'开头的查询失效Query Functions
查询函数(Query Functions)
tsx
// 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()
},
})tsx
// 查询函数接收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('获取失败')
return response.json()
},
})
// 使用signal实现自动取消
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal })
return response.json()
},
})queryOptions Helper
queryOptions 工具函数
Create reusable, type-safe query configurations:
tsx
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)创建可复用、类型安全的查询配置:
tsx
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)Queries (useQuery)
查询(useQuery)
Basic Usage
基础用法
tsx
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} />
}tsx
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} />
}Query Options
查询选项
tsx
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' },
})tsx
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' },
})Mutations (useMutation)
变更(useMutation)
Basic Usage
基础用法
tsx
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>
)
}tsx
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>
)
}Mutation State
变更状态
tsx
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({ ... })tsx
const {
mutate, // 触发后无需等待
mutateAsync, // 返回Promise
isPending, // 变更正在进行中
isError,
isSuccess,
isIdle, // 尚未触发
data, // 成功响应数据
error, // 错误对象
reset, // 将状态重置为idle
variables, // 传递给mutate的变量
status, // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })Optimistic Updates
乐观更新
tsx
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'] })
},
})tsx
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'] })
},
})Optimistic Updates on Lists
列表的乐观更新
tsx
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)
},tsx
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)
},Query Invalidation
查询失效
tsx
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'] })tsx
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'] })Infinite Queries
无限查询
tsx
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>
)
}tsx
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>
)
}Parallel Queries
并行查询
tsx
// 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),
}),
})
}tsx
// 多个独立查询会自动并行运行
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),
}),
})
}Dependent Queries
依赖查询
tsx
// 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
})
}tsx
// 使用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, // 仅当用户数据加载完成后才运行
})
}Paginated Queries
分页查询
tsx
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>
)
}tsx
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>
)
}Suspense Integration
Suspense 集成
tsx
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 },
],
})
}tsx
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 },
],
})
}Prefetching
预获取
tsx
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
})tsx
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页
})SSR & Hydration
SSR 与 水合
Server-Side Prefetching
服务端预获取
tsx
// 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>
)
}tsx
// 服务端组件或加载器
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>
)
}Streaming SSR (React Server Components)
流式SSR(React服务端组件)
tsx
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>
)
}tsx
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>
)
}QueryClient API
QueryClient API
tsx
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 })tsx
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 })Testing
测试
tsx
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()
})tsx
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('获取待办事项', 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('渲染待办事项', () => {
const queryClient = new QueryClient()
queryClient.setQueryData(['todos'], mockTodos)
render(
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
)
expect(screen.getByText('Todo 1')).toBeInTheDocument()
})TypeScript Patterns
TypeScript 模式
Typing Query Functions
为查询函数添加类型
tsx
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[] | undefinedtsx
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[] | undefinedTyping Errors
为错误添加类型
tsx
// 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
}
}tsx
// 默认错误类型是Error
const { error } = useQuery<Todo[], AxiosError>({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 或全局注册
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError
}
}Query Options Pattern (Recommended)
查询选项模式(推荐)
tsx
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 })tsx
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 })Advanced Patterns
高级模式
Window Focus Refetching
窗口焦点重新获取
tsx
// 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()
})tsx
// 全局禁用
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()
})Network Mode
网络模式
tsx
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',
})tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 'online'(默认):仅在线时获取
// 'always':始终获取(适合本地优先应用)
// 'offlineFirst':尝试获取,离线时使用缓存
networkMode: 'offlineFirst',
})Query Cancellation
查询取消
tsx
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'] })tsx
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
// signal是AbortSignal - 在组件卸载或键变更时自动取消
const res = await fetch('/api/todos', { signal })
return res.json()
},
})
// 手动取消
queryClient.cancelQueries({ queryKey: ['todos'] })Persistence
持久化
tsx
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
})tsx
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小时
})Best Practices
最佳实践
- Use helper for type-safe, reusable query configurations
queryOptions - Structure query keys hierarchically for granular invalidation
- Set appropriate - 0 means always refetch on mount (default), increase for less dynamic data
staleTime - Use (not
placeholderData) for keeping previous page data during paginationinitialData - Prefer when using Suspense boundaries for cleaner component code
useSuspenseQuery - Use for dependent queries, not conditional hook calls
enabled - Always invalidate after mutations - don't rely solely on optimistic updates
- Cancel queries in before optimistic updates to prevent race conditions
onMutate - Use in route loaders instead of
ensureQueryDatafor immediate accessprefetchQuery - Set in tests to avoid timeout issues
retry: false - Don't destructure the query result if you need to pass it around (breaks reactivity)
- Use for derived data instead of transforming in the component
select - Keep query functions pure - they should only fetch, not cause side effects
- Use in tests to prevent cache cleanup during assertions
gcTime: Infinity
- 使用工具函数创建类型安全、可复用的查询配置
queryOptions - 层级化结构查询键以实现细粒度的失效控制
- 设置合适的- 0表示挂载时始终重新获取(默认值),对于动态性较低的数据可以增大该值
staleTime - 在分页时使用(而非
placeholderData)来保留上一页的数据initialData - **使用Suspense边界时优先选择**以简化组件代码
useSuspenseQuery - 使用处理依赖查询,而非条件式调用钩子
enabled - 变更后始终使查询失效 - 不要仅依赖乐观更新
- 在中取消查询后再执行乐观更新以避免竞态条件
onMutate - **在路由加载器中使用**而非
ensureQueryData以立即获取数据prefetchQuery - **测试中设置**以避免超时问题
retry: false - 如果需要传递查询结果,不要解构它(会破坏响应式)
- 使用处理派生数据而非在组件中转换
select - 保持查询函数纯净 - 它们应该只负责获取数据,不要产生副作用
- **测试中设置**以防止断言期间缓存被清理
gcTime: Infinity
Common Pitfalls
常见陷阱
- Using when you mean
initialData(initialData counts as "fresh" data)placeholderData - Not providing for infinite queries (required in v5)
initialPageParam - Calling hooks conditionally (violates React rules)
- Not cancelling queries before optimistic updates (race conditions)
- Setting higher than
staleTime(data gets garbage collected while "fresh")gcTime - Forgetting to wrap tests with
QueryClientProvider - Using same instance across tests (shared state)
QueryClient - Not awaiting in mutation callbacks when order matters
invalidateQueries
- 当你需要的是时使用了
placeholderData(initialData会被视为“新鲜”数据)initialData - 无限查询未提供(v5中是必填项)
initialPageParam - 条件式调用钩子(违反React规则)
- 乐观更新前未取消查询(导致竞态条件)
- 设置得比
staleTime大(数据在“新鲜”时被垃圾回收)gcTime - 测试时忘记用包裹
QueryClientProvider - 测试间使用同一个实例(共享状态)
QueryClient - 当顺序很重要时,在变更回调中未等待完成
invalidateQueries