首页
/ TanStack Query Angular 实战:用 initialData 为 injectQuery 预置初始数据,跳过首屏 Loading

TanStack Query Angular 实战:用 initialData 为 injectQuery 预置初始数据,跳过首屏 Loading

2026-09-06 21:16:04作者:卓炯娓

在 Angular 中使用 TanStack Query(@tanstack/angular-queryinjectQuery)时,"查询挂上就立刻转圈"是常见的首屏体验问题。本文围绕 Angular 框架文档 initial-query-data 展开,系统讲解如何通过 initialDatastaleTimeinitialDataUpdatedAt 三个配置项,为查询预置初始数据、精确控制"何时重新拉取",并进一步演示懒求值的 initialData 函数、从缓存派生初始数据、以及按数据新鲜度做条件预置等进阶用法。读完本文,你能在 Angular 服务或组件中完整落地"有缓存就秒出、过期才回源"的数据初始化方案,并理解其背后 @tanstack/query-core 的状态初始化与陈旧度判定机制。

给查询预先准备数据的三种途径

TanStack Query 提供了"声明式"与"命令式"两类提前备数据的思路:

  • 声明式:在 injectQuery 的配置中直接提供 initialData,当该 key 在缓存中为空时用它预填充缓存,从而跳过初始 loading 状态;
  • 命令式:通过 queryClient 的 API 在需要之前手动操作缓存——用 queryClient.query 预取数据,或用 queryClient.setQueryData 直接把数据放入缓存。相关 API 详见 QueryClient 参考文档

声明式的 initialData 适合"应用里已经持有这份数据"的场景(例如路由状态、父查询结果、SSR 注入等)。

initialData 预置数据并跳过 loading 状态

当你的应用已经拥有某份数据时,可以直接把它交给 initialData,查询创建后不会进入 loading 状态,result.data() 立即可用:

result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
}))

在 Angular 中,这段代码写在服务或组件实例内即可执行(injectQuery 依赖注入上下文,参见 inject-query.ts)。一个值得注意的类型层细节:injectQuery 针对提供了 initialData 的重载会返回 DefinedCreateQueryResult<TData, TError>,而非普通的 CreateQueryResult(见 inject-query.ts#L65-L78)。也就是说,一旦声明了 initialData,TypeScript 会推断出"数据一定存在",data 信号的类型不再是可空——这正是"跳过初始 loading"在类型系统上的体现。

重要initialData 会被持久化写入缓存。因此不推荐用它塞入占位符、部分数据或不完整数据;如果只是想在加载期间显示占位内容,应使用 placeholderQueryData 方案。

staleTimeinitialDataUpdatedAt:决定预置数据"多新鲜"

initialData 默认被视为"刚刚拉取的最新数据",因此它会直接影响 staleTime 的判定。围绕这一点,有三种典型配置:

配置一:只有 initialData,没有 staleTime 默认 staleTime: 0 意味着数据立即可判定为陈旧,因此组件或服务实例一旦创建,查询会立即重新拉取——预置数据只是"先展示一帧":

// Will show initialTodos immediately, but also immediately refetch todos
// when an instance of the component or service is created
result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
}))

配置二:加上 staleTime 数据会被视为"新鲜"同样长的时间,仿佛是刚刚从 queryFn 拿到的。例如 staleTime: 1000 时,1 秒之内即使重新挂载也不会触发 refetch,只有遇到交互事件(窗口聚焦、网络恢复、组件重新挂载等)且数据已过新鲜期才会重新拉取:

// Show initialTodos immediately, but won't refetch until
// another interaction event is encountered after 1000 ms
result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
  staleTime: 1000,
}))

配置三:initialDataUpdatedAt 精确标注数据时间戳。 如果你的 initialData 其实并不新鲜,staleTime alone 就不够准确——这时应传入 initialDataUpdatedAt:一个毫秒级 JS 时间戳(Date.now() 同款),标明这份初始数据本身的最后更新时间。注意如果你的数据源给的是 Unix 秒级时间戳,需要乘以 1000 转换:

// Show initialTodos immediately, but won't refetch until
// another interaction event is encountered after 1000 ms
result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
  staleTime: 60 * 1000, // 1 minute
  // This could be 10 seconds ago or 10 minutes ago
  initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052
}))

这个选项让 staleTime 回归本职——定义"数据需要多新"——同时让查询自行决定:如果 initialData 的时间戳已经比 staleTime 更老,挂载时就会直接 refetch。上面的例子中,数据必须在 1 分钟内是新鲜的;若初始数据是 10 分钟前更新的,查询会判定其过期并立即回源。

如果你希望把数据当预取数据处理(而不是初始数据),更推荐先用 queryClient 的查询 API 把缓存填好,这样 staleTime 的语义就与 initialData 解耦了。

源码视角:dataUpdatedAt 是怎么初始化的

从源码结构看,上述行为的实现集中在 @tanstack/query-core 的状态构造函数 getDefaultState 中(query.ts#L745-L780):

const data =
  typeof options.initialData === 'function'
    ? (options.initialData as InitialDataFunction<TData>)()
    : options.initialData

const hasData = data !== undefined

const initialDataUpdatedAt = hasData
  ? typeof options.initialDataUpdatedAt === 'function'
    ? options.initialDataUpdatedAt()
    : options.initialDataUpdatedAt
  : 0

return {
  data,
  dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0,
  ...
  status: hasData ? 'success' : 'pending',
  fetchStatus: 'idle',
}

可以印证两点:其一,有 initialData 时初始 status 直接是 'success'dataUpdatedAt 默认取 Date.now()(即"视为刚刚获取"),所以 staleTime: 0 时挂载即 refetch;其二,提供了 initialDataUpdatedAt 时,它会被写入 dataUpdatedAt。而陈旧度判定 isStaleByTime 正是基于该时间戳计算(query.ts#L314-L330):return !timeUntilStale(this.state.dataUpdatedAt, staleTime)——dataUpdatedAt 距今超过 staleTime 即为陈旧,查询随即在下次观察时回源。相关类型定义见 types.ts#L259-L260initialData 可以是值或函数,initialDataUpdatedAt 可以是数字或返回数字的函数。

用函数形式的 initialData 做懒计算

如果获取初始数据的开销较大(读取大缓存、解析存储、做复杂查找),又不想在每次响应式执行时重复付出代价,可以把 initialData 写成函数。该函数只在查询初始化、创建默认状态时执行一次(即上面源码中 getDefaultState 的调用时机),从而节省内存与 CPU:

result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: () => getExpensiveTodos(),
}))

从其他查询的缓存派生初始数据

一个常见场景:你已有 ['todos'] 列表查询的缓存,现在要打开某个 todo 的详情查询。与其让详情查询从零开始加载,可以直接从列表缓存中找出那条记录作为详情查询的初始数据:

result = injectQuery(() => ({
  queryKey: ['todo', this.todoId()],
  queryFn: () => fetch('/todos'),
  initialData: () => {
    // Use a todo from the 'todos' query as the initial data for this todo query
    return this.queryClient
      .getQueryData(['todos'])
      ?.find((d) => d.id === this.todoId())
  },
}))

带上 initialDataUpdatedAt 的缓存派生

从缓存派生数据时,源查询的数据往往已经偏旧。此时不建议用人为拉高的 staleTime 来"骗"过立即 refetch,而是把源查询的 dataUpdatedAt 原样传给 initialDataUpdatedAt,让详情查询基于真实的新鲜度自行判断是否回源:

result = injectQuery(() => ({
  queryKey: ['todos', this.todoId()],
  queryFn: () => fetch(`/todos/${this.todoId()}`),
  initialData: () =>
    queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()),
  initialDataUpdatedAt: () =>
    queryClient.getQueryState(['todos'])?.dataUpdatedAt,
}))

条件式初始数据:旧到一定程度就不用缓存了

如果源查询的数据实在太旧,你宁愿干脆放弃缓存、从"硬加载"状态开始拉取。这时改用 queryClient.getQueryState 拿到完整的查询状态,用 state.dataUpdatedAt 做新鲜度门槛判断:

result = injectQuery(() => ({
  queryKey: ['todo', this.todoId()],
  queryFn: () => fetch(`/todos/${this.todoId()}`),
  initialData: () => {
    // Get the query state
    const state = queryClient.getQueryState(['todos'])

    // If the query exists and has data that is no older than 10 seconds...
    if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) {
      // return the individual todo
      return state.data.find((d) => d.id === this.todoId())
    }

    // Otherwise, return undefined and let it fetch from a hard loading state!
  },
}))

注意这里返回 undefined 是刻意为之:initialData 求值结果为 undefined 时,getDefaultState 会走 hasData === false 分支,状态回到 'pending',查询进入正常的加载流程(参见上文 query.ts#L758 附近的 hasData 逻辑)。

Angular 集成要点与验证依据

  • injectQuery 的实现在 inject-query.ts:它通过 runInInjectionContext 在给定(或当前)注入上下文中创建 QueryObserver,因此示例中 this.todoId()this.queryClient 这类依赖实例状态的写法在信号化的 Angular 应用中天然成立;
  • injectQuery 传入的选项函数运行在响应式上下文中(文档注释中说明其类似 computed),queryKeyenabled 等都可以引用信号;initialData 的求值则只发生在查询初始化时,两者时机不同,写懒函数时不必担心被响应式重算反复触发;
  • 提供的选项类型 DefinedInitialDataOptions / UndefinedInitialDataOptions 定义在 query-options.ts,分别对应"有初始数据(返回 Defined 结果)"和"无初始数据(返回普通结果)"两种重载;
  • 行为验证可参考测试文件 inject-query.test.ts 中围绕 initialData 的用例,以及 query-options.test-d.ts 中对 initialData 类型的编译期断言。

小结

场景 配置要点 效果
跳过首屏 loading initialData 立即展示数据,status 直接为 success
立即展示但挂载即回源 initialDatastaleTime 默认 0) 显示一帧后立即 refetch
展示并延迟回源 initialData + staleTime: 1000 数据视为刚拉取,1 秒内不 refetch
数据本身可能偏旧 initialData + staleTime + initialDataUpdatedAt 按真实时间戳判定新鲜度,过旧则挂载即回源
初始数据获取昂贵 initialData: () => ... 仅初始化时求值一次
用其他查询的缓存派生 getQueryData / getQueryState + initialDataUpdatedAt 继承源查询的新鲜度,可加新鲜度门槛做条件预置

再次强调核心原则:initialData 会持久化进缓存,只应放真实、完整的数据;占位展示请用 placeholderData(见 placeholder-query-data)。掌握以上配置组合,你就能在 Angular 应用中把"初始数据 + 陈旧度"的控制权完全握在自己手里。

登录后查看全文
热门项目推荐
相关项目推荐