Query 入门:安装与 QueryClient
本教程共 38 篇 · 第 4 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
4. Query 入门:安装与 QueryClient
本节目标:把 TanStack Query 装到项目里,跑通第一个查询,搞懂 QueryClient、Provider、useQuery 三者的关系,学完你能让数据请求在项目里跑起来。
4.1 安装
Query 的包名是 @tanstack/react-query,用你习惯的包管理器装:
# npm
npm i @tanstack/react-query
# pnpm
pnpm add @tanstack/react-query
# yarn
yarn add @tanstack/react-query
# bun
bun add @tanstack/react-query
环境要求:
- React 18.0 或更高(v5 用了
useSyncExternalStore,老版本 React 不行) - 浏览器 Chrome 91+ / Firefox 90+ / Edge 91+ / Safari 15+
NoteQuery 同时兼容 ReactDOM 和 React Native,用法一样。React Native 里监听焦点和网络状态要额外配置,后面遇到再说。
4.2 顺便装个 ESLint 插件
官方有个 ESLint 插件,能帮你抓依赖缺失、API 误用这类问题。建议开发时就装上:
npm i -D @tanstack/eslint-plugin-query
然后在 ESLint 配置里启用,具体规则可以看官方文档。这个插件能避免一些很隐蔽的坑,比如查询键里漏了依赖变量。
4.3 三大核心概念
写代码之前,先认识 Query 的三个核心概念,整个库都是围着它们转的:
- 查询(Query):用
useQuery发起一个异步数据请求,带个唯一 key 标识 - 变更(Mutation):用
useMutation修改服务端数据(增删改) - 失效(Invalidation):用
queryClient.invalidateQueries标记缓存过期,触发重新请求
这三个概念构成了 Query 的骨架。后面十几章都在讲怎么用它们。
4.4 创建 QueryClient
QueryClient 是整个 Query 的「大脑」,所有缓存、查询实例都归它管。第一步是创建它:
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient()
就这么简单。不传参数就是全用默认配置。但实际项目里你通常想调几个默认值:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 数据多久算「过期」,默认 0(立即过期)
staleTime: 1000 * 60, // 1 分钟
// 多久没人用就回收,默认 5 分钟
gcTime: 1000 * 60 * 10, // 10 分钟
// 失败重试次数,默认 3
retry: 3,
// 窗口聚焦时是否重新请求,默认 true
refetchOnWindowFocus: false,
},
},
})
这几个参数后面会专门讲,这里先知道能在 defaultOptions.queries 里统一配就行。
Tip
defaultOptions里配的是全局默认。单个useQuery还能覆盖这些值,局部优先级高于全局。
4.5 挂载 Provider
QueryClient 创建好了,得让整个应用都能用到它。靠的是 QueryClientProvider:
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
QueryClientProvider 把 queryClient 通过 React Context 往下传,应用里任何地方都能用 useQueryClient() 拿到它。
挂载位置一般在应用最外层,确保所有组件都在 Provider 内部。和 React Router、Redux Provider 这些放一起就行。
4.6 第一个 useQuery
Provider 挂好后,就能在组件里用 useQuery 发请求了。看个完整例子:
import { useQuery } from '@tanstack/react-query'
function Example() {
const { isPending, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query').then((res) =>
res.json(),
),
})
if (isPending) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>Stars: {data.stargazers_count}</strong>
</div>
)
}
拆解一下:
queryKey: ['repoData']— 这个查询的唯一标识,是个数组queryFn— 实际发请求的函数,必须返回 PromiseisPending— 还没拿到数据时为 trueerror— 出错时的错误对象data— 成功时的数据
v5 的 useQuery 只接受对象签名。如果你看到老教程写 useQuery(['key'], fn),那是 v3/v4 的写法,v5 已经移除了。
Warning老项目从 v4 升 v5,有个 codemod 能帮你自动改签名。但改完一定要 review,codemod 有边界情况处理不了。
4.7 用 useQueryClient 拿客户端
除了 useQuery,还有个常用 hook 是 useQueryClient,用来在组件里拿到那个 queryClient 实例:
import { useQueryClient } from '@tanstack/react-query'
function Todos() {
const queryClient = useQueryClient()
// 手动触发刷新、失效、读缓存等操作
const refetch = () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
return <button onClick={refetch}>刷新</button>
}
后面讲失效、乐观更新、缓存操作时,这个 hook 会频繁用到。
4.8 一个完整的小例子
把前面的东西串起来,写个能跑的 todo 列表:
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
const queryClient = useQueryClient()
// 查询:拿 todo 列表
const query = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
return res.json()
},
})
// 变更:新增 todo
const mutation = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
return res.json()
},
onSuccess: () => {
// 新增成功后,让 todo 列表失效重新请求
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
if (query.isPending) return <div>加载中...</div>
if (query.isError) return <div>出错了:{query.error.message}</div>
return (
<div>
<ul>
{query.data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({ id: Date.now(), title: '洗衣服' })
}}
>
新增 Todo
</button>
</div>
)
}
export default App
这个例子涵盖了三大核心概念:useQuery 查询、useMutation 变更、invalidateQueries 失效。Mutation 的具体用法后面会细讲,这里先看个全貌。
4.9 几个新手坑
坑一:忘记挂 Provider。 useQuery 在 Provider 外面用会报错。报错信息还算清楚,但第一次遇到会懵。
坑二:QueryClient 放在组件内部。 这样每次组件重渲染都会 new 一个新的 client,缓存全丢。正确做法是放在组件外层,或者用 useState 包一层:
// 推荐写法:放组件外
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
// SSR 场景:每个请求一个独立 client,避免数据串
function App() {
const [queryClient] = useState(() => new QueryClient())
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
坑三:fetch 不抛错。 原生 fetch 在 HTTP 状态码非 2xx 时不会自动 throw。你得手动判断 response.ok,不然 Query 会以为请求成功,把错误响应当数据存进缓存:
queryFn: async () => {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error('请求失败:' + response.status)
}
return response.json()
}
Tip如果嫌每个
queryFn都写response.ok判断麻烦,可以封装个统一的fetcher函数,或者直接用 axios(它会自动抛错)。
4.10 小结
Query 的上手就三步:装包、建 QueryClient、挂 Provider。然后就能在任意组件里用 useQuery 发请求了。
记住几个要点:QueryClient 放组件外层别放里面;v5 只支持对象签名;fetch 要手动判断 response.ok。
下一篇我们细讲 useQuery 的各种用法。