Skip to content
高级查询模式

高级查询模式

本章涵盖并行查询、依赖查询、动态查询、预取、轮询、查询取消等高级模式。


并行查询(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。前者避免不必要的请求但保持数据新鲜度;后者会让用户看到过时数据。