TanStack Query Angular 实战:用 initialData 为 injectQuery 预置初始数据,跳过首屏 Loading
2026/9/7 18:38:13 网站建设 项目流程

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

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

在 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其实并不新鲜,staleTimealone 就不够准确——这时应传入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-L260:initialData可以是值或函数,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类型的编译期断言。

小结

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

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

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询