首页 / TanStack 生态入门教程 / useQuery 基础:发起查询

TanStack 生态入门教程

useQuery 基础:发起查询

本教程共 38 篇 · 第 5 篇 · 更新于 2026-07-27 · 约 9 分钟阅读

TanStackTanStack 生态入门教程useQueryqueryKeyqueryFnenabled轮询refetchInterval

5. useQuery 基础:发起查询

本节目标:掌握 useQuery 的对象签名、queryKey 和 queryFn 怎么写、怎么禁用查询、怎么做轮询,学完你能发起各种形态的查询请求。

5.1 useQuery 的对象签名

v5 的 useQuery 只接受对象参数,不再支持数组参数。这是 v5 最大的破坏性变更之一。

// v3/v4 旧写法,v5 已移除
useQuery(['todos'], fetchTodos, { enabled: false })

// v5 唯一支持的写法
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  enabled: false,
})

对象签名的好处是参数顺序不再重要,可读性也好很多。queryKeyqueryFn 是必填的,其他都是可选。

调用 useQuery 后返回一个结果对象,最常用的几个字段:

const result = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

// result 包含:
// - status: 'pending' | 'error' | 'success'
// - isPending: boolean  (没数据)
// - isError: boolean
// - isSuccess: boolean
// - data: 数据
// - error: 错误对象
// - isFetching: boolean  (正在请求,包括后台)

5.2 queryKey:查询的唯一标识

queryKey 是个数组,用来唯一标识一份缓存数据。最简单的情况就一个字符串:

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

需要带参数时,把参数放进数组:

// 查单个 todo
useQuery({
  queryKey: ['todo', 5],
  queryFn: () => fetchTodoById(5),
})

// 带筛选条件的列表
useQuery({
  queryKey: ['todos', { status: 'done', page: 1 }],
  queryFn: fetchTodoList,
})

有个铁律:queryFn 用到的变量,必须放进 queryKey。因为 Query 靠 queryKey 判断「数据变没变」。变量变了但 key 没变,Query 不会重新请求。

function Todo({ todoId }) {
  // todoId 必须进 key
  const result = useQuery({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodoById(todoId),
  })
}
Note

queryKey 还充当查询函数的「依赖」。变量变了,key 变了,Query 自动重新请求。这点和 useEffect 的依赖数组思路类似。

关于 queryKey 的设计还有不少讲究,下一章会专门讲。

5.3 queryFn:实际请求函数

queryFn 是真正发请求的函数,必须返回一个 Promise。Promise 要么 resolve 数据,要么 throw 错误。

几种合法写法:

// 直接传函数引用
useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })

// 箭头函数包一层
useQuery({
  queryKey: ['todos', todoId],
  queryFn: () => fetchTodoById(todoId),
})

// async/await
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async () => {
    const data = await fetchTodoById(todoId)
    return data
  },
})

注意一个细节:resolve 的值不能是 undefined。如果你想存「啥也没有」,用 null 代替:

// 错误:resolve undefined 会被当成失败
queryFn: async () => {
  if (notFound) return undefined
  return data
}

// 正确:用 null 表示空
queryFn: async () => {
  if (notFound) return null
  return data
}

queryFn 能拿到 queryKey

queryFn 接收一个上下文对象,里面有 queryKey。这意味着你可以从 key 里取参数,不用闭包:

useQuery({
  queryKey: ['todos', { status, page }],
  queryFn: ({ queryKey }) => {
    const [_key, { status, page }] = queryKey
    return fetchTodoList({ status, page })
  },
})

这样写的好处是 queryFn 可以抽出去复用,不依赖组件作用域的变量。这个上下文对象还有 signal(用于取消)、meta 等字段,后面会用到。

5.4 渲染查询结果

拿到结果后,常见的渲染模式是三段式:先判 pending,再判 error,最后默认就是 success:

function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })

  if (isPending) {
    return <span>加载中...</span>
  }

  if (isError) {
    return <span>出错了:{error.message}</span>
  }

  // 走到这里说明 isSuccess
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

不喜欢布尔值的话,用 status 字符串也行,效果一样:

const { status, data, error } = useQuery({...})

if (status === 'pending') return <span>加载中...</span>
if (status === 'error') return <span>出错了:{error.message}</span>
// status === 'success'

TypeScript 在你判完 pendingerror 之后,会自动把 data 收窄成非 undefined 类型,不用手动断言。

5.5 禁用查询:enabled

默认情况下,useQuery 挂载就自动请求。但有时你想手动控制时机,用 enabled: false

const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
  enabled: false,  // 不自动请求
})

// 手动触发
return <button onClick={() => refetch()}>获取数据</button>

enabled: false 时:

  • 不会自动请求
  • 不会后台刷新
  • 不会响应 invalidateQueries(除非有缓存数据)
  • 可以用 refetch() 手动触发

懒查询(Lazy Queries)

更实用的场景是条件启用。比如搜索框,用户输了关键词才请求:

function Todos() {
  const [filter, setFilter] = React.useState('')

  const { data } = useQuery({
    queryKey: ['todos', filter],
    queryFn: () => fetchTodos(filter),
    // filter 为空时不请求
    enabled: !!filter,
  })

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="输入关键词搜索"
      />
      {data && <TodosTable data={data} />}
    </div>
  )
}

filter 一旦有值,enabled 变 true,查询自动触发。这种「延迟到条件满足才请求」的模式叫懒查询。

Tip

懒查询时,isPending 一开始就是 true(因为没数据),但你又没在请求,这时候用 isPending 判断 loading 会出问题。v5 里 isLoading 的语义是 isPending && isFetching,只在「真正在首次请求」时为 true。后面状态详解那章会细讲。

TypeScript 用 skipToken

TypeScript 用户有个更类型安全的禁用方式:skipToken

import { skipToken, useQuery } from '@tanstack/react-query'

const { data } = useQuery({
  queryKey: ['todos', filter],
  queryFn: filter ? () => fetchTodos(filter) : skipToken,
})

skipToken 是个特殊标记值,效果等同 enabled: false,但 queryFn 的类型签名不会被破坏。

Warning

用了 skipToken 之后,refetch() 就不能用了,会报 Missing queryFn 错误。需要手动触发的场景还是用 enabled: false

5.6 轮询:refetchInterval

想让查询定时刷新?用 refetchInterval,单位毫秒:

useQuery({
  queryKey: ['prices'],
  queryFn: fetchPrices,
  refetchInterval: 5000,  // 每 5 秒请求一次
})

只要还有组件在用这个查询(active observer),轮询就会持续。所有组件卸载了,轮询也停。

根据状态调整间隔

refetchInterval 可以传函数,根据查询结果决定间隔。比如任务没完成时频繁轮询,完成了就停:

useQuery({
  queryKey: ['job', jobId],
  queryFn: () => fetchJobStatus(jobId),
  refetchInterval: (query) => {
    // 任务完成就停止轮询
    if (query.state.data?.status === 'complete') return false
    return 2000  // 否则每 2 秒查一次
  },
})

返回 false 就停止轮询。如果后续查询结果变了又该轮询,它会自动恢复。

后台轮询

默认情况下,浏览器标签页切到后台时轮询会暂停。如果想让它切后台也继续,加 refetchIntervalInBackground

useQuery({
  queryKey: ['portfolio'],
  queryFn: fetchPortfolio,
  refetchInterval: 30000,
  refetchIntervalInBackground: true,  // 后台也轮询
})
Note

refetchIntervalstaleTime 是独立的。即使数据是 fresh 的(没过期),轮询照样按自己的节奏跑。别把这两个概念搞混了。

5.7 v5 的几个变化

既然讲到 useQuery,提几个 v5 的破坏性变更,免得你照老教程踩坑:

  • 数组签名移除useQuery(key, fn, options) 不再支持,必须用对象
  • 回调移除onSuccessonErroronSettleduseQuery 移除了。这些副作用请用 useEffect 配合 data/error 实现,或者改用 useMutation(mutation 还保留这些回调)
  • remove 方法移除query.remove() 没了,改用 queryClient.removeQueries({ queryKey })
  • isLoading 语义变了:v5 里 isLoading 等于 isPending && isFetching,和 v4 的 isInitialLoading 一样
Warning

如果你看到教程里在 useQuery 里写 onSuccess: (data) => {...},那是 v4 及之前的写法。v5 里这么写不会报错但回调不会执行,坑了不少人。要监听数据变化,用 useEffect

const { data } = useQuery({...})

useEffect(() => {
  if (data) {
    // 数据来了,做点什么
  }
}, [data])

5.8 小结

useQuery 的基础就这些:对象签名、queryKey 当唯一标识和依赖、queryFn 返回 Promise、enabled 控制启用时机、refetchInterval 做轮询。

记住几条:queryFn 用到的变量必须进 queryKey;resolve 别用 undefined 用 null;v5 回调没了改用 useEffect;skipTokenenabled: false 更类型安全但不能 refetch。

下一篇深入讲 queryKey 的设计。