TanStack Query 之 Initial Query Data:为 Vue Query 查询预填充缓存的完整指南
【免费下载链接】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
在 TanStack Query(Vue Query)中,查询缓存是数据流的中心。很多时候,你的应用在发起网络请求之前就已经持有部分数据(例如路由状态、状态管理库中的快照、上一屏查询的缓存结果),如果能让这些数据直接进入查询缓存并跳过 loading 状态,体验会好很多。本篇指南以docs/framework/vue/guides/initial-query-data.md为核心,系统讲解如何通过initialData以声明式方式为查询预填充缓存,深入剖析它与staleTime、initialDataUpdatedAt的配合逻辑,并结合仓库源码(query.ts、useBaseQuery.ts)揭示底层实现原理。读完本文,你将掌握"从零缓存秒开"与"从旧缓存优雅过渡"两种实战方案,并能正确区分initialData与placeholderData的适用场景。
一、为什么需要 Initial Query Data:三种预填充缓存的途径
在真正需要某条查询数据之前,你有多种方式把数据"喂"给缓存。TanStack Query 官方指南将其分为两大类:
- 声明式(Declaratively):在查询选项中提供
initialData,如果缓存为空,就用这份数据预填充缓存; - 命令式(Imperatively):
- 使用
queryClient.prefetchQuery(或usePrefetchQuery)预先拉取数据并写入缓存,详见 prefetching 指南; - 使用
queryClient.setQueryData手动把数据直接放进缓存,同样详见 prefetching 指南。
- 使用
两者的本质区别在于:命令式方式在缓存被填充时就已经"完成了一次取数";而声明式方式只是把一份你手上已有的数据作为查询的初始状态。下文重点展开声明式方案。
二、使用initialData预填充查询
当你的应用在发起请求之前就已经拥有某条查询的初始数据时,可以直接把它传给查询选项。这种情况下,initialData会让查询跳过初始的 loading 状态,首次渲染即可展示数据:
import { useQuery } from '@tanstack/vue-query' // 假设 initialTodos 已在应用中可用 const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos').then((res) => res.json()), initialData: initialTodos, })重要提示:
initialData会被持久化到缓存(persisted to the cache),因此不建议向它传入占位性的、部分的或不完整的数据;这类场景应当使用placeholderData(详见 placeholder-query-data 指南)。
从源码层面看,这一行为由 query.ts 中的getDefaultState函数落实:当查询实例初始化时,它会读取options.initialData并写入查询状态,只要data !== undefined,查询的初始status就是'success'而非'pending',dataUpdatedAt也会被设置。这就是"跳过 loading 状态"这一行为的底层依据。
2.1staleTime与initialDataUpdatedAt的交互
默认情况下,initialData会被当作"完全新鲜"的数据,仿佛刚刚从查询函数取回。这意味着它会直接影响staleTime对数据的判断。三种配置对应三种行为:
场景一:只传initialData,不配置staleTime
默认staleTime为0,查询挂载后会立即重新请求:
// 会立即展示 initialTodos,但挂载后也会立刻重新请求 todos const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos').then((res) => res.json()), initialData: initialTodos, })场景二:initialData+staleTime: 1000
数据在1000毫秒内被视为新鲜,与刚从查询函数取回的效果一致:
// 立即展示 initialTodos,但 1000 毫秒内遇到其他交互事件也不会触发重新请求 const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos').then((res) => res.json()), initialData: initialTodos, staleTime: 1000, })场景三:initialDataUpdatedAt——当你的初始数据"没那么新鲜"
前两种配置都隐含了一个假设:initialData是刚刚生成的。但实际场景中,这份数据可能是 10 秒前甚至 10 分钟前缓存在应用里的。此时应使用initialDataUpdatedAt,传入这份数据最近一次更新的 JavaScript 时间戳(毫秒),即Date.now()返回的那类数值:
// 立即展示 initialTodos;若数据距今超过 1 分钟,挂载时就会重新请求 const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos').then((res) => res.json()), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // 这份数据可能是 10 秒前更新的,也可能是 10 分钟前更新的 initialDataUpdatedAt: initialTodosUpdatedTimestamp, // 例如 1608412420052 })注意:如果拿到的 Unix 时间戳(秒),需要乘以
1000转换成 JavaScript 毫秒时间戳。
initialDataUpdatedAt让staleTime回归其本来用途——"数据需要多新鲜"——同时让查询在挂载时自行判断:若initialData的年龄已超过staleTime,就触发重新请求;否则直接使用。底层逻辑在 query.ts 中一目了然:dataUpdatedAt优先取initialDataUpdatedAt(支持函数形式),未提供时才回退到Date.now();若没有数据则为0。
如果你更愿意把这份数据当作预取(prefetched)数据来处理,官方建议改用
prefetchQuery等命令式 API 预先填充缓存,从而让staleTime的配置与initialData解耦(详见 prefetching 指南)。
2.2 Initial Data 函数:延迟到查询初始化时执行
如果获取初始数据的过程代价较高(例如需要遍历大量内存数据),或你不想让它在每次渲染时都执行,可以把函数传给initialData。该函数只在查询初始化时执行一次,从而节省内存与 CPU:
const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos').then((res) => res.json()), initialData: () => getExpensiveTodos(), })这与getDefaultState的实现完全对应:query.ts 中会先判断typeof options.initialData === 'function',是函数则立即调用一次取结果。类型层面,InitialDataFunction<T> = () => T | undefined定义于 types.ts,且initialData与initialDataUpdatedAt均声明为"值或函数"两种形态(见 types.ts)。
需要强调的是,这里的"只执行一次"指的是查询实例初始化这一时刻,而不是每次组件渲染。这与
queryFn每次请求都会执行不同,是initialData函数的核心价值。
三、从其他查询的缓存中派生初始数据
3.1 基础用法:用列表缓存作为详情页初始数据
一个非常典型的场景:你有一个 todos 列表查询,当用户点击某一条进入详情页时,完全可以用列表缓存里的对应项作为详情查询的初始数据,让详情页瞬间呈现,而不是白屏等待:
import { useQuery, useQueryClient } from '@tanstack/vue-query' const queryClient = useQueryClient() const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`).then((res) => res.json()), initialData: () => { // 从 'todos' 查询的缓存中取出一条 todo,作为本条查询的初始数据 return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, })3.2 进阶:配合initialDataUpdatedAt继承源查询的新鲜度
从缓存取初始数据意味着:源查询(上面的['todos'])很可能已经过时。与其用一个人为设定的staleTime来强行阻止立即重新请求,官方建议把源查询的dataUpdatedAt透传给initialDataUpdatedAt。这样查询实例就能基于真实数据年龄自行判断是否需要重新请求:
const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`).then((res) => res.json()), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, })这里initialDataUpdatedAt也使用了函数形式,每次求值时读取源查询的当前dataUpdatedAt,保证时间戳始终与缓存真实状态同步。类型定义中initialDataUpdatedAt?: number | (() => number | undefined)明确支持这种写法(见 types.ts)。
3.3 条件式初始数据:源缓存太旧就回退到加载态
如果源查询太旧,你甚至根本不想使用这份缓存数据,直接回退到硬加载状态。此时用queryClient.getQueryState读取源查询的更多信息(包括state.dataUpdatedAt时间戳),自行判断新鲜度即可:
const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`).then((res) => res.json()), initialData: () => { // 获取源查询的状态 const state = queryClient.getQueryState(['todos']) // 如果查询存在,且其数据距今不超过 10 秒…… if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // 返回对应的单个 todo return state.data.find((d) => d.id === todoId) } // 否则返回 undefined,让查询进入硬加载状态(status 为 pending) }, })这段代码的本质是:initialData函数返回undefined时,getDefaultState中hasData为false,查询状态直接回到status: 'pending'(见 query.ts),也就是正常的加载流程。
四、Vue 场景下的响应式细节
本指南文档面向 Vue 框架,useQuery的用法与 React 版保持一致的选项模型(initialData、initialDataUpdatedAt、staleTime等选项均直接继承自QueryObserverOptions,见 useQuery.ts)。但在 Vue 组合式 API 中有几个值得注意的点:
- 必须在
setup()或作用域内调用:useBaseQuery在开发模式下会检查getCurrentScope(),若在setup()或 effect scope 之外调用会发出内存泄漏警告(见 useBaseQuery.ts)。 - 选项可以是响应式的:
UseQueryOptions支持MaybeRefOrGetter,因此initialData、queryKey等都可以传入 ref 或 getter;useBaseQuery内部通过cloneDeepUnref解包响应式值后再传给client.defaultQueryOptions(见 useBaseQuery.ts)。这意味着基于todoIdref 动态取缓存的写法天然生效。 - 返回值为响应式 ref:
useQuery返回的结果(如data、isLoading、isSuccess)均以 ref 形式暴露,在模板中会被自动解包。
五、进一步阅读
- 想深入理解
initialData与placeholderData的差异与取舍,参见 placeholder-query-data 指南; - 想掌握命令式预取缓存的完整姿势,参见 prefetching 指南;
- 想了解查询的新鲜度与失效机制,参见 query-invalidation 指南 与 important-defaults 指南。
六、总结
initialData是 TanStack Query(Vue Query)中"数据已在手"场景的最优解:它声明式地将已有数据写入缓存、跳过 loading 状态,且与staleTime、initialDataUpdatedAt组合后能精确控制"何时重新请求"。其底层实现集中在 query.ts 的getDefaultState(初始状态构建)与 types.ts 的选项类型定义中;Vue 侧的响应式封装则在 useBaseQuery.ts 中完成。掌握本文的三种initialDataUpdatedAt用法(直接时间戳、源查询dataUpdatedAt透传、条件式新鲜度判断),即可在真实项目中写出既秒开又不过时的新缓存策略。
【免费下载链接】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),仅供参考