2.1 useQuery 的形状
useQuery 接收一个配置对象,最核心的两个字段是 queryKey 和 queryFn;返回一个包含 data、状态标志和方法的对象。
const result = useQuery({
queryKey: ["todos"], // 缓存索引
queryFn: fetchTodos, // 返回 Promise 的函数
});
// result: { data, error, status, isLoading, isError, isSuccess, refetch, ... }
2.2 核心概念
- queryKey
- 查询的唯一标识,必须是一个数组。TanStack Query 用它作为缓存的 key、去重的依据、失效的目标。key 变化会触发重新获取(类似 useEffect 依赖数组)。
- queryFn
- 实际获取数据的函数,必须返回一个 resolve 数据或 reject 错误的 Promise。它接收一个 context 参数,内含
queryKey和signal(用于取消)。 - status
- 数据的三态:
'pending'(还没数据)、'error'(获取失败)、'success'(有数据)。是 data 是否可用的权威判断。 - fetchStatus
- 与 status 正交的另一维度:
'fetching'(正在请求)、'paused'(暂停,如离线)、'idle'(空闲)。区分"有没有数据"和"此刻是否在网络请求"。 - isLoading
- 等价于
isPending && isFetching——首次加载、还没有任何缓存数据时为 true。用于显示初始 loading 骨架。 - isFetching
- 只要有网络请求进行中就为 true,包括后台刷新。适合做顶部细进度条这类"正在同步"提示。
- enabled
- 布尔开关,为 false 时查询不会自动执行。用于依赖查询(等前置数据就绪)或条件查询(用户点了才查)。
isLoading vs isFetching 别搞混
isLoading:从来没有数据、正在首次加载 → 显示骨架屏。isFetching:任何请求进行中(含后台刷新)→ 显示"同步中"小圈。切走再回来触发后台刷新时,isLoading 是 false(有缓存),isFetching 是 true。
2.3 queryFn 与 context
queryFn 会收到一个 context,其中 queryKey 让你把 key 里的参数直接用于请求,signal 支持自动取消:
function useTodo(id: number) {
return useQuery({
queryKey: ["todo", id],
queryFn: async ({ queryKey, signal }) => {
const [, todoId] = queryKey; // 从 key 解构参数
const res = await fetch(`/api/todos/${todoId}`, { signal });
if (!res.ok) throw new Error("请求失败");
return res.json() as Promise<Todo>;
},
});
}
queryFn 必须抛错,不能吞
fetch 对 4xx/5xx 不会 reject!你必须手动检查 res.ok 并 throw,否则 TanStack Query 会把错误响应当成成功数据缓存,isError 永远为 false。用 axios 则会自动 reject。
2.4 queryKey 设计原则
queryKey 是数组,遵循"从宽到窄"层级结构,越靠前越通用:
// ✅ 层级化:便于批量失效
["todos"] // 所有 todos
["todos", "list", { status: "done" }] // 已完成列表
["todos", "detail", id] // 单条详情
// 对象放在数组里时,key 顺序无关(自动序列化)
["todos", { status: "done", page: 1 }] // 与 { page, status } 等价
好处:失效 ["todos"] 会连带失效所有以它开头的 key,非常适合"改了一条 todo,让整个 todo 相关缓存都重取"。
2.5 依赖查询 enabled
当 B 查询需要 A 查询的结果作为参数时,用 enabled 等 A 就绪:
const { data: user } = useQuery({
queryKey: ["user", email],
queryFn: () => getUserByEmail(email),
});
const userId = user?.id;
const { data: projects } = useQuery({
queryKey: ["projects", userId],
queryFn: () => getProjectsByUser(userId!),
enabled: !!userId, // userId 存在才发请求
});
enabled 为 false 时的状态
此时 status === 'pending' 但 fetchStatus === 'idle'。所以不能只用 isLoading 判断——它可能永远 true。v5 里可用 isPending && isFetching 或直接看 fetchStatus。
2.6 常用返回字段速查
| 字段 | 类型 | 含义 |
|---|---|---|
| data | T | undefined | 成功时的数据 |
| error | Error | null | 失败时的错误对象 |
| status | 'pending' | 'error' | 'success' | 数据状态 |
| isLoading | boolean | 首次加载中(无缓存) |
| isFetching | boolean | 任意请求进行中 |
| isSuccess / isError | boolean | status 的布尔快捷方式 |
| refetch | () => Promise | 手动重新获取 |
| dataUpdatedAt | number | 数据最后更新时间戳 |
2.7 完整示例:待办列表
function TodoList() {
const { data, isLoading, isError, error, isFetching, refetch } =
useQuery({
queryKey: ["todos", "list"],
queryFn: fetchTodos,
});
if (isLoading) return <Skeleton />;
if (isError) return <p>错误:{error.message}</p>;
return (
<div>
{isFetching && <span>同步中…</span>}
<button onClick={() => refetch()}>刷新</button>
<ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
</div>
);
}
2.8 小结与下一步
- useQuery 三要素:queryKey(索引)、queryFn(返回 Promise)、返回状态对象。
- status 管"有没有数据",fetchStatus 管"是否在请求",两者正交。
- queryKey 用层级数组,便于批量失效;参数放进 key 里驱动重取。
- enabled 组织依赖查询,等前置数据就绪再执行。
- 下一章深入 缓存机制——staleTime、gcTime 与后台刷新的时机。