Chapter 02

useQuery 基础

掌握 useQuery 的三大要素——queryKey、queryFn、返回状态,并学会用 enabled 组织依赖查询。

2.1 useQuery 的形状

useQuery 接收一个配置对象,最核心的两个字段是 queryKeyqueryFn;返回一个包含 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 参数,内含 queryKeysignal(用于取消)。
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.okthrow,否则 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 常用返回字段速查

字段类型含义
dataT | undefined成功时的数据
errorError | null失败时的错误对象
status'pending' | 'error' | 'success'数据状态
isLoadingboolean首次加载中(无缓存)
isFetchingboolean任意请求进行中
isSuccess / isErrorbooleanstatus 的布尔快捷方式
refetch() => Promise手动重新获取
dataUpdatedAtnumber数据最后更新时间戳

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