TanStack Query 数据层

共 17 题
📑 题目列表 17 题
#
★★★

1. TanStack Query v5 useSuspenseQuery/useSuspenseInfiniteQuery 的工程价值

TanStack Query v5 中 useSuspenseQuery 与 useSuspenseInfiniteQuery 相比普通 useQuery 在工程上有什么价值?

  • Suspense 模式下数据加载与渲染的协调
  • 取消手动管理 loading 状态的三态样板
  • 与 React 并发特性(Suspense、transition)的配合

useSuspenseQuery 让组件在数据未就绪时直接 suspend,由最近的 Suspense 边界负责展示 fallback,从而消除了组件内部手写的 isLoading 分支。useSuspenseInfiniteQuery 除了自动分页,还支持按页码/游标逐页 suspend,配合 initialPageParamgetNextPageParam 实现滚动加载。工程上它让数据加载更像"声明式资源",减少每个页面重复的三态(loading/error/data)样板,并能使整个渲染树在 load 期间被协调器优先处理,配合 useTransition 在旧新内容间平滑过渡,避免瀑布流中的闪烁。

普通 useQuery 保留 loading 状态是"命令式"的,而 Suspense 版本把加载状态交给 React 协调器,语义更接近稳定渲染,配合 useTransition 还能在旧内容与新内容间平滑过渡。

const { data } = useSuspenseQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) });
// 无需 if (isLoading),由 <Suspense fallback={...}> 负责
#
★★★

2. Query 的 SSR Streaming 与 dehydrate/hydrate 的工程价值

TanStack Query 的 SSR Streaming 与 dehydrate/hydrate 机制在工程上有什么价值?

  • 服务端预取数据并随 HTML 传输
  • 流式渲染与增量数据加载的配合
  • 客户端水合时复用缓存避免重复请求

dehydrate 把服务端 QueryClient 缓存序列化为 HTML 中内嵌的 JSON,hydrate 在客户端重建缓存,使页面首屏的数据无需再次请求即可渲染。SSR Streaming 则允许服务端边渲染边把 chunk 推到客户端,配合 prefetchQuery 让数据在流式边界到达时即可用。工程价值是显著降低首屏延迟与 FCP/TTFB,同时避免客户端与服务端重复拉取同一数据造成的瀑布流。

关键是把"服务端已经取到的数据"共享给客户端,这是数据一致性(hydration mismatch)与性能(避免重复请求)的平衡点。

// 服务端
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const dehydrated = dehydrate(queryClient);
// 客户端
hydrate(queryClient, dehydrated);
#
★★★

3. Query v5 的服务器状态缓存、queryKey、staleTime/gcTime

TanStack Query v5 中服务器状态缓存、queryKey 与 staleTime/gcTime 的概念与工程应用是什么?

  • queryKey 的确定性序列化与缓存身份
  • staleTime(数据新鲜度)与 gcTime(缓存回收)的区别
  • 失效与重取的时机

queryKey 是缓存的主键,必须可序列化且语义稳定,任何可能影响数据的参数都应纳入 key。staleTime 定义数据在多长时间内视为新鲜,新鲜期内不触发后台重取,避免同一数据在高频切换时重复请求;gcTime 定义缓存被移除前保留多久,影响内存占用与"切回时是否立即显示旧数据"。v5 中默认 staleTime 为 0、gcTime 为 5 分钟,工程上常按业务调整:低频数据可设更长 staleTime 减少请求。

staleTime 是"还能不能用旧数据",gcTime 是"缓存还存不存"。两者语义不同,组合使用控制请求频率与内存。

useQuery({ queryKey: ['user', userId], queryFn: fetchUser, staleTime: 60_000, gcTime: 300_000 });
#
★★★

4. TanStack Router 的 router.invalidate 与 loaderDeps 依赖跟踪的工程价值

TanStack Router 的 router.invalidate 与 loaderDeps 依赖跟踪在工程上有什么价值?

  • 路由级数据失效
  • loaderDeps 声明 loader 依赖的响应式参数
  • 与 Query 缓存配合的失效策略

router.invalidate 会触发重新加载当前路由树中所有 loader 或使相关路由数据失效,常用于数据变更后统一刷新。loaderDeps 声明 loader 依赖的响应式参数(如 search 参数),当该参数变化时自动重新执行 loader,从而实现"依赖驱动"的数据获取,避免手动 watch 的样板。工程价值是让数据失效与刷新逻辑集中、可预测,减少脏数据。

loaderDeps 优于在 loader 内部直接读 search,因为声明式依赖让 Router 能精确判断何时重跑 loader,而不是每次 search 变化都重跑或漏跑。

const route = createRoute({
  path: '/user',
  loaderDeps: ({ search }) => ({ userId: search.userId }),
  loader: ({ deps }) => fetchUser(deps.userId),
});
#
★★★

5. Router 与 Query 缓存的协作

TanStack Router 与 TanStack Query 如何在工程中协作管理数据获取?

  • loader 中调用 queryClient.ensureQueryData
  • 路由导航与查询缓存的配合
  • 失效与预取的一致性

常见模式是在 Router 的 loader 中调用 queryClient.ensureQueryData({ queryKey, queryFn }),它会在缓存命中且新鲜时直接返回,否则发起请求。这样既利用 Router 的导航生命周期(beforeLoad/loader),又复用 Query 的缓存、重试与失效能力。数据变更后通过 router.invalidate()queryClient.invalidateQueries() 触发刷新。两者协作避免重复请求,并让"路由进入即数据就绪"与"数据新鲜度"两套逻辑各司其职。

Router 管"导航与参数",Query 管"数据缓存与生命周期",协作的关键是确保同一个 queryKey 在两处被一致使用。

loader: ({ context }) => context.queryClient.ensureQueryData({ queryKey: ['post', id], queryFn: () => fetchPost(id) })
#
★★

6. gcTime/staleTime/refetchOnWindowFocus 的策略组合

如何组合 gcTime、staleTime 与 refetchOnWindowFocus 制定数据刷新策略?

  • 三个配置的语义
  • 数据新鲜度与后台刷新策略
  • 请求频率与一致性的平衡

典型组合:staleTime 设为一个业务上可接受的时间窗(如 30s-5min),窗口内数据视为新鲜不重取;gcTime 通常大于 staleTime,保证切回时缓存仍可立即展示;refetchOnWindowFocus 默认 true,可在窗口重新聚焦时后台刷新已过期数据,让用户看到最新结果。工程上应根据数据重要性调整:实时数据 staleTime 设短并开启窗口聚焦刷新,静态数据可设长 staleTime 并关闭聚焦刷新以省请求。

三者协同决定"何时显示旧数据、何时后台刷新、缓存保留多久",是请求频率与数据实时性的平衡。

new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, gcTime: 5 * 60_000, refetchOnWindowFocus: true } } });
#
★★

7. Query 的 networkMode: 'offlineFirst' 在 PWA 的工程价值

Query 的 networkMode: 'offlineFirst' 在 PWA 中有何工程价值?

  • networkMode 的三种取值
  • 离线时缓存优先于网络
  • 与 Service Worker 的配合

networkMode 有 'online'(默认,离线时暂停重试)、'always'(从不检查在线状态)、'offlineFirst' 三种。offlineFirst 模式下,Query 在离线时先尝试使用缓存数据,只有缓存缺失时才进入等待网络恢复的状态,不立即报错。对 PWA 的价值是:离线时页面仍能展示已缓存数据,体验接近原生 App,网络恢复后自动继续。它需要配合 Service Worker 缓存与持久化缓存(persist)使用。

offlineFirst 让"离线优先"策略落地,使缓存命中优先于网络错误,是离线体验的关键一环。

useQuery({ queryKey: ['doc', id], queryFn: fetchDoc, networkMode: 'offlineFirst' });
#
★★

8. Query Key Factory 模式与类型安全

Query Key Factory 模式是什么?它如何带来类型安全?

  • 集中管理 queryKey 的构造
  • 消除 key 拼写错误的隐患
  • 与 queryOptions 的类型推导

Query Key Factory 把 queryKey 的构造集中到一个对象中,定义如 postKeys.all(), postKeys.detail(id), postKeys.list(filter) 等工厂函数,统一管理 key 的形状。它带来类型安全:key 的 scope 与参数是强类型的,避免在任何地方手写字符串 key 导致拼写不一致、失效时 key 对不上等问题。配合 queryOptions helper 还能让 queryFn 与 key 的类型自动关联,减少 any。

核心价值是"一处定义、处处复用",让 key 的创建、读取、失效保持一致,是大型项目可维护性的关键。

export const postKeys = {
  all: ['posts'] as const,
  detail: (id: number) => [...postKeys.all, 'detail', id] as const,
};
queryClient.invalidateQueries({ queryKey: postKeys.detail(id) });
#
★★

9. 无限查询 getNextPageParam 与游标设计,基于游标 vs 基于页码的分页策略与缓存键设计

useInfiniteQuery 的 getNextPageParam 与游标设计,以及基于游标 vs 基于页码的分页策略与缓存键设计是怎样的?

  • getNextPageParam 返回下一页参数的语义
  • 游标分页与页码分页的差异
  • 缓存键如何区分不同分页状态

getNextPageParam 接收上一页数据,返回下一页的请求参数(页码或游标),返回 undefined 表示没有更多页。基于页码分页参数简单但插入/删除会导致后续数据偏移;基于游标分页以稳定标识(如 id 或时间戳)定位,数据变动不影响后续页,更利于增量加载。缓存键设计上,分页列表的 key 应包含筛选条件但不包含页码,页码数据存于 pageParams 数组,这样不同分页状态共享同一列表缓存。

游标分页在数据频繁增删的场景更稳健,页码分页实现简单;缓存键把"筛选条件"与"页码"分离是设计关键。

useInfiniteQuery({
  queryKey: ['posts', filter],
  queryFn: ({ pageParam }) => fetchPage({ ...filter, cursor: pageParam }),
  initialPageParam: undefined,
  getNextPageParam: (last) => last.nextCursor ?? undefined,
});
#
★★

10. TanStack Router 的 defaultPreloadStaleTime 与缓存策略的工程应用

TanStack Router 的 defaultPreloadStaleTime 在缓存策略中如何应用?

  • preload 机制的触发时机
  • defaultPreloadStaleTime 定义预取数据的新鲜度
  • 避免预取数据过期即失效

Router 的 preload 会在链接进入视口或 hover 时主动预取目标路由的数据。defaultPreloadStaleTime 定义预取得到的数据在多久内视为新鲜,决定导航到该路由时是直接使用预取数据还是重新请求。工程上把它设为一个合理值(如 30s),既能在用户点击时立即使用预取数据提升导航速度,又不会让过久的数据被当作新鲜而展示陈旧内容。

preload 是"提前取",defaultPreloadStaleTime 是"预取的数据能新多久",两者配合决定导航时机与数据新鲜度。

const router = createRouter({ routeTree, defaultPreloadStaleTime: 30_000 });
#
★★

11. TanStack Router 与 TanStack Query 的 loader 数据获取集成

TanStack Router 与 TanStack Query 在 loader 数据获取上如何集成?

  • loader 中使用 queryClient 取数
  • ensureQueryData 与缓存复用
  • 路由上下文注入 queryClient

集成方式是把 QueryClient 注入 Router 的 routeContext,loader 中通过 context 拿到 queryClient 并调用 ensureQueryData 获取数据。这样路由进入时数据已就绪,且与 Query 缓存共享,避免首屏重复请求与 loading 闪烁。数据变更后调用 invalidateQueries 或 router.invalidate 刷新。这是官方推荐的二者协作模式。

让 Query 承担数据层、Router 承担导航层,通过 context 注入调用点,实现职责分离与数据复用。

const routerContext = { queryClient };
const router = createRouter({ routeTree, context: routerContext });
// loader: ({ context }) => context.queryClient.ensureQueryData({...})
#
★★

12. TanStack Query 的 queryClient.getQueryData 与 SSR 序列化协作

queryClient.getQueryData 如何与 SSR 序列化协作?

  • 同步读取缓存数据
  • 服务端预取后传输
  • 避免重复请求

getQueryData 同步返回缓存中指定 key 的数据(不存在返回 undefined),适用于已确定数据存在时直接读取。在 SSR 场景,服务端先 prefetchQuery 填充缓存,再通过 dehydrate 序列化,客户端 hydrate 后 getQueryData 即可同步拿到数据,无需再请求。协作的关键是链路为"服务端取数 → 序列化 → 客户端水合 → 同步读取",保证首屏即数据完整。

getQueryData 是"读取已有缓存"的同步 API,与 dehydrate/hydrate 的序列化链路配合,实现 SSR 数据直出。

const data = queryClient.getQueryData(['user', id]);
#
★★

13. Query 的 select 与 Transformer 在数据归一化的应用

Query 的 select 选项与 Transformer 在数据归一化中如何应用?

  • select 对返回数据做投影/变换
  • 避免重复计算
  • 归一化(normalize)处理

select 在观察层对缓存数据做变换,组件只拿到需要的形状,避免每个组件都做重复转换。它返回新引用,若派生逻辑昂贵可配合 structuralSharing 或维护派生缓存避免不必要重渲染。归一化场景下,可把列表数据通过 select 转为按 id 索引的 map,或把单条记录从缓存中提取,与 useState/useMemo 相比 select 更贴合数据流。

select 是"查询结果投影",让数据在源头完成归一化与形状裁剪,组件消费更简单。

useQuery({ queryKey: ['users'], queryFn: fetchUsers, select: (users) => users.filter(u => u.active) });
#
★★

14. Optimistic Update、Infinite Query、Suspense Query

如何理解并应用 Optimistic Update、Infinite Query 与 Suspense Query?

  • 乐观更新的写入路径与回滚
  • 无限查询的分页加载
  • Suspense 查询的加载协调

Optimistic Update 在 mutation 发起前先乐观写入缓存,接口失败时回滚,提升交互感知;Infinite Query 通过 getNextPageParam 实现滚动/分页加载,数据以 pages 数组累积;Suspense Query 让组件在数据未就绪时挂起由 Suspense 边界处理。三者分别解决"写入体验、分页加载、加载状态协调",工程上常组合使用(如乐观更新的列表 + 无限滚动 + Suspense 边界)。

三个模式各司其职:乐观更新优化写入、无限查询优化大数据列表、Suspense 优化加载状态。

const { mutate } = useMutation({
  mutationFn: toggleLike,
  onMutate: async (id) => { await qc.cancelQueries({ queryKey: ['post', id] }); const prev = qc.getQueryData(['post', id]); qc.setQueryData(['post', id], {...prev, liked: !prev.liked}); return { prev }; },
  onError: (_, id, ctx) => qc.setQueryData(['post', id], ctx.prev),
});
#
★★

15. queryKey factory pattern (https://tkdodo.eu/blog/effective-react-query-keys) 的工程价值

tkdodo 提出的 queryKey factory pattern 的工程价值是什么?

  • 集中管理 key 构造
  • 一致性与失效可靠性
  • 类型安全与可维护性

该模式把 queryKey 的构造集中为工厂函数,如 userKeys.all()userKeys.detail(id),并遵循"全局 key → 子 key → 参数"的层级结构。工程价值:key 构造、读取、失效三处引用同一函数,杜绝拼写不一致导致失效失败;key 的层级结构便于按前缀批量失效(invalidateQueries 前缀匹配);类型安全减少 any。它是大型项目管理一堆 queryKey 的公认最佳实践。

核心是"单一来源",让 key 的定义与使用保持一致,是失效机制可靠性的前提。

export const userKeys = {
  all: ['users'] as const,
  detail: (id: number) => [...userKeys.all, 'detail', id] as const,
};
#
★★

16. offline mutation 队列与 networkMode 恢复重放,离线操作持久化与网络恢复后的冲突解决策略

offline mutation 队列与 networkMode 恢复重放,以及离线操作持久化与网络恢复后的冲突解决策略是什么?

  • 离线时 mutation 的排队与持久化
  • 网络恢复后按序重放
  • 冲突解决(版本号、最后写入、服务端校验)

离线时 mutation 无法立即执行,需要把操作加入队列并持久化(localStorage/IndexedDB),网络恢复后按顺序重放。networkMode 影响重试行为:'online' 在离线时暂停。冲突解决策略包括:携带客户端版本号做乐观锁、服务端校验返回冲突标记、或采用"最后写入胜出"并在冲突时提示用户。工程上需保证重放幂等(每操作有唯一 id),并处理仅部分操作成功的部分成功状态。

离线队列的关键是"持久化 + 有序重放 + 幂等 + 冲突处理",复杂度高,需结合业务场景权衡。

// 离线时把 mutation 存入队列并持久化,网络恢复后逐条重放
const queue = JSON.parse(localStorage.getItem('offlineQueue') ?? '[]');
queue.push({ id: crypto.randomUUID(), payload });
localStorage.setItem('offlineQueue', JSON.stringify(queue));
#

17. TanStack Start 客户端导航、预取(preload)与 Query 缓存的协作

TanStack Start 中客户端导航、预取(preload)与 Query 缓存如何协作?

  • Start 基于 TanStack Router 的导航与预取
  • preload 与 Query 缓存融合
  • 全栈框架下的数据流

TanStack Start 复用 TanStack Router 的客户端导航与 preload 机制,在链接进入视口/hover 时预取目标路由 loader 数据,loader 内通过 queryClient.ensureQueryData 与 Query 缓存融合。导航时数据已就绪,直接渲染,避免 loading。它打通了"客户端导航 → 预取 → Query 缓存 → SSR 水合"的完整链路,是 Start 相比传统 SPA 在体验上的优势。

Start 把 Router 的导航层级与 Query 的数据缓存通过网络预取串起来,让导航与数据同步就绪。

// routes/posts.$id.tsx
loader: ({ context, params }) => context.queryClient.ensureQueryData({ queryKey: ['post', params.id], queryFn: () => fetchPost(params.id) })