首页
/ TanStack Query Angular 实战:用 placeholderData 让查询在数据到达前即刻渲染

TanStack Query Angular 实战:用 placeholderData 让查询在数据到达前即刻渲染

2026-09-06 20:30:04作者:史锋燃Gardner

在 Angular 中使用 injectQuery 时,查询初始处于 pending 状态,界面往往只能显示加载指示器。placeholderData 选项允许查询在真实数据尚未取回前就表现得像"已有数据"——它类似 initialData,但不会被持久化到缓存,非常适合拥有部分数据(甚至模拟数据)先行渲染、真实数据在后台继续拉取的场景。读完本篇,你将掌握 Angular 中三种占位数据写法(静态值、记忆化值、函数式、从缓存派生)的完整用法与底层状态机原理,并知道如何用 isPlaceholderData 标志区分占位数据与真实数据。

什么是占位数据

占位数据的核心价值在于:让查询跳过 pending 状态、直接以 success 状态启动,因为此时查询已经有可以展示的 data——哪怕这只是"占位"数据。为了区分占位数据与真实数据,查询结果上会额外携带 isPlaceholderData: true 标志。

一个典型场景来自官方文档的说明:单篇博客文章的查询,可以从"博客列表"父查询中取出只包含标题和正文片段的"预览版"数据,作为详情页查询的占位数据。你不会希望把这份不完整的数据持久化到详情页查询的结果里,但它可以让内容布局尽可能快地上屏,而完整对象仍在后台获取。

在 TanStack Query 中,有两种方式为查询提供占位数据:

  • 声明式:在查询配置中直接传入 placeholderData,当缓存为空时预填充查询结果;
  • 命令式:通过 queryClient 使用 placeholderData 选项进行预取(Prefetch)。

从源码看,占位逻辑集中在核心包的 QueryObserver 中:只有当 options.placeholderData !== undefined、当前 data === undefinedstatus === 'pending' 三个条件同时满足时,占位数据才会被应用;一旦应用,状态会被改写为 success,并设置 isPlaceholderData = true。这解释了为什么占位查询不会触发 isLoading 分支。

占位数据作为静态值

最简单的用法是直接把一个已有数据传给 placeholderData

class TodosComponent {
  result = injectQuery(() => ({
    queryKey: ['todos'],
    queryFn: () => fetch('/todos'),
    placeholderData: placeholderTodos,
  }))
}

查询挂载后立即以 placeholderTodos 渲染,queryFn 在后台执行;一旦解析完成,data 被替换为真实数据,isPlaceholderData 变为 false。仓库中的测试用例 inject-query.test.ts 完整验证了这一生命周期:

it('should show placeholderData until queryFn resolves and then expose real data', async () => {
  // ...
  readonly query = injectQuery(() => ({
    queryKey: key,
    queryFn: () => sleep(10).then(() => 'real-data'),
    placeholderData: 'placeholder',
  }))
  // 断言:初始 data: placeholder、isPlaceholderData: true、isSuccess: true
  // 等待 queryFn 解析后:data: real-data、isPlaceholderData: false
})

这证实了文档中"占位数据期间查询处于 success 状态"的描述在 Angular 适配器中同样成立。

占位数据的记忆化

如果生成占位数据的过程开销较大(例如构造大量模拟对象),不想每次渲染都重新执行,可以在组件中先把值计算一次再传入。Angular 中没有 React 的 useMemo,但从源码结构看,injectQuery 的回调运行在与 computed 类似的响应式上下文中,因此可以使用信号来缓存一次性计算结果:

class TodosComponent {
  // 仅在信号首次被读取时生成一次
  readonly placeholder = computed(() => generateFakeTodos())

  result = injectQuery(() => ({
    queryKey: ['todos'],
    queryFn: () => fetch('/todos'),
    placeholderData: this.placeholder(),
  }))
}

更进一步,核心层本身就内置了占位数据的记忆化机制:在 queryObserver.ts 中,若上一次结果也是占位数据(prevResult?.isPlaceholderData)且 placeholderData 选项引用未变,则直接复用 prevResult.data 并跳过 select 重新执行,避免对同一份占位数据重复计算。

占位数据作为函数

placeholderData 还可以是函数形式,可以接收"上一个"成功查询的数据与查询元信息:

class TodosComponent {
  result = injectQuery(() => ({
    queryKey: ['todos', id()],
    queryFn: () => fetch(`/todos/${id}`),
    placeholderData: (previousData, previousQuery) => previousData,
  }))
}

这里的 id 是一个信号,读取它的当前值作为查询键的一部分。这个模式在 queryKey 发生变化(例如从 ['todos', 1] 变为 ['todos', 2])时尤为有用:新 key 没有缓存数据,但函数式占位数据可以返回旧 key 的 previousData,从而在数据"过渡期"继续展示旧内容,而不是显示加载转圈。这正是 分页查询指南 中翻页过渡的常用技巧。

函数两个参数的来源可以在 queryObserver.ts 中确认:核心会调用 placeholderData(this.#lastQueryWithDefinedData?.state.data, this.#lastQueryWithDefinedData),即把"最近一个有数据的查询"的 state.data 和查询实例本身传入。

注意:previousData 来自上一个有数据的查询,如果应用里同时存在多个查询,务必确认该函数被调用时"上一个查询"确实是你要复用的那个,否则会拿到意料之外的数据。

从缓存派生占位数据

在某些场景下,可以直接从另一个已缓存查询的结果中查找、派生出占位数据。典型例子是"博客列表 → 博客详情":列表查询只缓存每篇文章的标题和摘要(预览版),详情页查询则可以用它作为占位数据:

export class BlogPostComponent {
  postId = input.required<number>()
  queryClient = inject(QueryClient)

  result = injectQuery(() => ({
    queryKey: ['blogPost', this.postId()],
    queryFn: () => fetch(`/blogPosts/${this.postId()}`),
    placeholderData: () => {
      // 使用 'blogPosts' 查询中较小的/预览版 blogPost
      // 作为本 blogPost 查询的占位数据
      return this.queryClient
        .getQueryData(['blogPosts'])
        ?.find((d) => d.id === this.postId())
    },
  }))
}

实现要点:

  • 通过 inject(QueryClient) 注入客户端实例(可参考 queryOptions 指南查询指南 了解 QueryClient 的提供方式);
  • queryClient.getQueryData(['blogPosts']) 按 key 同步读取缓存,若列表查询尚未完成则返回 undefined,函数式占位数据返回 undefined 时查询回落到普通 pending 状态,不会报错;
  • postIdinput.required<number>() 声明并作为信号读取,保证路由参数变化时查询自动重建。

占位数据与 initialData 的边界

两者的关键差异在于缓存持久化:

  • initialData写入缓存并参与缓存生命周期(可被其他观察者复用、被持久化插件持久化);
  • placeholderData 只影响当前观察者(observer 级选项,参见 query-core 类型定义),真实数据取回后即被替换,不会污染缓存。

因此在"临时展示用、但不想让半截数据冒充缓存结果"的场景(如本文的列表预览 → 详情全文),应优先选择 placeholderData

小结

  • placeholderData 使 Angular 中的 injectQuery 跳过 pending,直接以 success + isPlaceholderData: true 启动,让界面即刻可渲染;
  • 静态值适合固定的骨架数据;函数形式 (previousData, previousQuery) 适合 queryKey 切换时平滑过渡;
  • 借助 queryClient.getQueryData 可以从缓存中的"兄弟查询"派生预览数据;
  • 核心层对占位数据有内置记忆化(queryObserver.ts),且 placeholderData 是观察者级选项、不进入缓存;
  • 模板中可用 query.isPlaceholderData() 区分占位与真实数据,据此控制骨架屏或"内容加载中"的视觉提示。
登录后查看全文
热门项目推荐
相关项目推荐