8.1 从"能用"到"优雅"
基础 useQuery 足够跑起来,但真实项目里你会遇到:只想要数据的一部分、要并行发一批查询、想在多处复用同一份查询配置、想让 TypeScript 帮你兜住类型。本章讲这些进阶模式。
8.2 核心概念
- select
- 数据转换/派生函数。它只影响本组件拿到的 data 形状,不改变缓存;且仅在原始数据或 select 函数变化时重算,可用于挑字段、计算派生值、减少重渲染。
- 并行查询 (Parallel Queries)
- 多个相互独立的查询同时发起。静态已知数量时直接写多个 useQuery;数量动态时用 useQueries。
- 依赖查询 (Dependent Queries)
- 后一个查询依赖前一个的结果,用 enabled 串起来(第 2 章讲过),本章看它与并行的组合。
- useQueries
- 接收一个查询配置数组,返回结果数组。适合"数量在运行时才确定"的场景,如根据 id 列表批量拉详情。可用 combine 合并结果。
- queryOptions
- v5 引入的辅助函数,把一份查询配置抽成可复用、类型完整的对象,供 useQuery、prefetchQuery、getQueryData 等共用同一份 key + fn,杜绝 key 拼错。
- combine
- useQueries 的选项,把多个结果聚合成单一派生值(如全部 isLoading、data 合并成一个数组),并做记忆化。
- 类型安全
- queryFn 的返回类型自动流入 data;配合 queryOptions 与泛型,从请求到渲染全程有类型,改后端字段后 TS 立刻报错。
8.3 select 转换与派生
// 只取需要的字段,减少无关渲染
const { data: names } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
select: (users) => users.map(u => u.name), // User[] → string[]
});
// 计算派生值:只有 count 变化才重渲染
const { data: doneCount } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
select: (todos) => todos.filter(t => t.done).length,
});
select 的性能红利
缓存里存的仍是完整数据,多个组件可各用不同 select 派生各自需要的切片。因为 select 结果被记忆化,只有派生值真正变了才触发该组件重渲染——大列表里挑一个字段时尤其有用。
8.4 并行查询
// 静态数量:直接并列,自动并行
const user = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const stats = useQuery({ queryKey: ["stats"], queryFn: fetchStats });
const feed = useQuery({ queryKey: ["feed"], queryFn: fetchFeed });
8.5 useQueries 动态并行
import { useQueries } from "@tanstack/react-query";
function Det123({ ids }: { ids: number[] }) {
const results = useQueries({
queries: ids.map((id) => ({
queryKey: ["todo", id],
queryFn: () => fetchTodo(id),
staleTime: 60_000,
})),
combine: (results) => ({
data: results.map(r => r.data),
isPending: results.some(r => r.isPending),
}),
});
if (results.isPending) return <Skeleton />;
return <ul>{results.data.map(t => <li key={t!.id}>{t!.title}</li>)}</ul>;
}
8.6 queryOptions 复用配置
把 key + fn + 选项抽成一份,随处复用,类型完整、key 不再手写易错:
import { queryOptions } from "@tanstack/react-query";
// queries/todo.ts —— 单一事实来源
export const todoQuery = (id: number) =>
queryOptions({
queryKey: ["todos", "detail", id],
queryFn: () => fetchTodo(id),
staleTime: 60_000,
});
// 组件里
const { data } = useQuery(todoQuery(5));
// 路由 loader 里预取,同一份配置
await queryClient.ensureQueryData(todoQuery(5));
// setQueryData 也用它推断类型,data 自动为 Todo
queryClient.setQueryData(todoQuery(5).queryKey, updated);
为什么 queryOptions 是 v5 的最佳实践
它把 queryKey 和 queryFn 绑成一体,避免"key 在这里写 ['todo', id],那里写 ['todos', id]"的经典 bug;同时 TS 能从 fn 推出 data 类型,让 setQueryData / getQueryData 全程类型安全。
8.7 各模式选型
| 需求 | 用法 |
|---|---|
| 转换/挑选数据 | select |
| 固定几个独立查询 | 并列多个 useQuery |
| 运行时才知数量 | useQueries |
| B 依赖 A 结果 | enabled + userId |
| 多处复用同一查询 | queryOptions 工厂 |
| 聚合多结果 | useQueries + combine |
8.8 端到端类型安全示例
interface Todo { id: number; title: string; done: boolean; }
const fetchTodos = async (): Promise<Todo[]> =>
(await fetch("/api/todos")).json();
const { data } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
// data 推断为 Todo[] | undefined,无需手动标注
data?.map(t => t.titel); // ❌ TS 立刻报错:拼错字段名
8.9 小结与下一步
- select 派生数据、减少重渲染,且不污染缓存。
- 并行查询直接并列;数量动态用 useQueries + combine。
- queryOptions 是 v5 复用查询配置的最佳实践,杜绝 key 拼错。
- queryFn 返回类型贯穿 data,TS 全程护航。
- 下一章讲 SSR 与框架集成——Next.js hydration、Vue/Solid Query。