高级查询模式
本章涵盖并行查询、依赖查询、动态查询、预取、轮询、查询取消等高级模式。
并行查询(Parallel Queries)
静态并行
直接写多个 useQuery——React 每次渲染都会执行所有 Hook:
function Dashboard() {
const { data: users } = useQuery({
queryKey: ['users'], queryFn: fetchUsers,
})
const { data: projects } = useQuery({
queryKey: ['projects'], queryFn: fetchProjects,
})
const { data: stats } = useQuery({
queryKey: ['stats'], queryFn: fetchStats,
})
// 三个请求同时发出,互不等待
}💡 最佳实践:数量固定的并行查询直接写多个
useQuery,简单直观。
动态并行 — useQueries
当查询数量在渲染期间才能确定时,用 useQueries:
function UserGroups({ userIds }: { userIds: number[] }) {
const userQueries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000,
})),
combine: (results) => {
// 汇总所有查询结果
return {
data: results.map(r => r.data).filter(Boolean),
isPending: results.some(r => r.isPending),
isError: results.some(r => r.isError),
}
},
})
if (userQueries.isPending) return <Spinner />
return <UserList users={userQueries.data} />
}Suspense 并行 — useSuspenseQueries
import { useSuspenseQueries } from '@tanstack/react-query'
function Dashboard() {
const [{ data: users }, { data: projects }] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
// data 不会是 undefined(由 Suspense 保证)
}⚠️
useSuspenseQueries不支持enabled选项。如需条件查询,用useSuspenseQuery+ 条件渲染替代。
依赖查询(Dependent / Enabled Queries)
一个查询依赖另一个查询的结果:
// 第一步:获取用户
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
// 第二步:用户就绪后才获取其项目
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user?.id, // user 的 id 存在时才执行
})依赖查询的执行时机
userId 变化
│
▼
useQuery(['user', userId]) 触发
│
▼
user 加载完成 (user.id 有值)
│
▼
enabled: !!user?.id 变为 true
│
▼
useQuery(['projects', user.id]) 自动触发🚨 陷阱:依赖查询中
queryFn使用了user!(非空断言)。这是因为enabled: !!user?.id保证了执行时 user 一定存在,但 TypeScript 无法跨enabled选项推断类型收窄。
预取(Prefetching)
在用户实际需要数据之前,提前将数据放入缓存:
queryClient.prefetchQuery
const queryClient = useQueryClient()
// 方案一:hover 时预取(intent-based prefetching)
function TodoLink({ id }: { id: number }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
staleTime: 10 * 60 * 1000, // 预取数据 10 分钟内不重取
})
}
return (
<Link to={`/todo/${id}`} onMouseEnter={prefetch}>
待办 #{id}
</Link>
)
}
// 方案二:列表页预取详情
function TodoList() {
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
// 列表加载后立即预取前 3 个详情
todos?.slice(0, 3).forEach(todo => {
queryClient.prefetchQuery({
queryKey: ['todo', todo.id],
queryFn: () => fetchTodo(todo.id),
})
})
}, [todos, queryClient])
}queryClient.prefetchInfiniteQuery
queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
pages: 3, // 预取前 3 页
})queryClient.setQueryData — 预先填充
你已经有了部分数据时,直接放入缓存:
// 从列表页跳转详情页时,传递已知数据
queryClient.setQueryData(['todo', todo.id], todo)
// 也可以用一个查询的数据填充另一个查询
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: fetchUser })
queryClient.setQueryData(['user-profile', id], user)轮询(Polling / Refetch Interval)
// 每 5 秒自动刷新
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 5_000, // 固定间隔
})
// 动态间隔:数据过期后加快频率
useQuery({
queryKey: ['task-status', taskId],
queryFn: () => checkTaskStatus(taskId),
refetchInterval: (data, query) => {
// 任务完成前每秒轮询,完成后停止
return data?.status === 'running' ? 1000 : false
},
})⚠️ 轮询在窗口失焦时仍会发送请求。如需在后台暂停,使用条件判断或结合
refetchOnWindowFocus关闭。
查询取消(Query Cancellation)
TanStack Query 会自动传入 AbortSignal 给你的查询函数。组件卸载、查询键变化或手动取消时,会触发 signal.abort():
useQuery({
queryKey: ['todos', id],
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos/${id}`, { signal })
// signal.aborted 被调用时,fetch 会抛出 AbortError
// TanStack Query 自动忽略 AbortError
return res.json()
},
})手动取消
// 取消所有 matching 查询的进行中请求
await queryClient.cancelQueries({ queryKey: ['todos'] })💡 最佳实践:始终将
signal传给fetch或其他支持 AbortController 的 API。这不仅节约带宽,还避免了过时请求覆盖新数据的竞态。
queryOptions 跨文件复用
queryOptions 的最大价值在于跨组件、跨页面复用查询定义:
// src/query/options/todoOptions.ts
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
import { todoKeys } from '../keys'
export function todoListOptions(filters: TodoFilters = {}) {
return queryOptions({
queryKey: todoKeys.list(filters),
queryFn: () => fetchTodos(filters),
staleTime: 30 * 1000,
})
}
export function todoDetailOptions(id: number) {
return queryOptions({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
staleTime: 5 * 60 * 1000,
})
}
export function todoInfiniteOptions() {
return infiniteQueryOptions({
queryKey: todoKeys.all,
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
}// 在组件中使用
function TodoPage() {
const { data } = useQuery(todoListOptions({ status: 'active' }))
}
function TodoDetailPage({ id }: { id: number }) {
const { data } = useQuery(todoDetailOptions(id))
}
// 在 loader 中预取(React Router v6 Data Router)
async function todoLoader() {
await queryClient.ensureQueryData(todoListOptions())
return null
}
// 在路由守卫中预取
function usePrefetchOnHover(id: number) {
const queryClient = useQueryClient()
return () => queryClient.prefetchQuery(todoDetailOptions(id))
}networkMode — 离线行为
v5 支持三种网络模式:
| 模式 | 行为 |
|---|---|
'online'(默认) |
离线时暂停请求,联网后自动恢复 |
'always' |
不管网络状态,始终尝试请求 |
'offlineFirst' |
先返回缓存(如有),同时发起请求;比 stale-while-revalidate 更激进 |
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
networkMode: 'offlineFirst',
})refetchOnMount / refetchOnWindowFocus 详解
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60_000,
// 组件每次挂载时是否重取 stale 数据
refetchOnMount: true, // 默认 true。false = 不重取;'always' = 无论 stale 与否都重取
// 用户切换回标签页时
refetchOnWindowFocus: true, // 默认 true
// 网络重连时
refetchOnReconnect: true, // 默认 true
})⚡ 性能提示:如果数据变化不频繁,设置
staleTime而不是关闭refetchOnWindowFocus。前者避免不必要的请求但保持数据新鲜度;后者会让用户看到过时数据。