Chapter 08

高级模式

用 select 转换数据、useQueries 动态并行、queryOptions 复用配置,把类型安全贯彻到底。

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 小结与下一步