TanStack 生态深化

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

1. TanStack Query v5 的 Suspense 集成与 Streaming SSR,如何与 React 19 / Solid.js 的并发渲染深度协同?

TanStack Query v5 的 Suspense 集成与 Streaming SSR 是如何与 React 19、Solid.js 的并发渲染深度协同的?

  • Query v5 的 suspense: true 模式与 useSuspenseQuery 的语义
  • Streaming SSR 下查询数据在服务端的预取与流式注入
  • 与 React 19/Solid 并发渲染(Suspense、startTransition)的配合机制

Query v5 把 Suspense 从实验特性升级为一等公民:useSuspenseQuery 在数据未就绪时抛出 Promise 让最近的 Suspense 边界挂起,数据到达后自动恢复渲染,省去 isPending 分支;服务端渲染时,通过 prefetchQuery 在服务端把查询结果写入 queryClient,配合 React 19 的 Streaming SSR,Suspense 边界可以在数据未就绪时先流式输出占位内容,数据就绪后由服务器流式补发,客户端 hydrate 时查询缓存已就绪、无二次请求。与 Solid.js 协同同理:Solid 的 Suspense 与资源机制让查询在服务端可等待、在客户端可切换,Query 提供统一的缓存层。协同的关键是"查询状态交给 Query 管理、渲染挂起交给框架的 Suspense 机制",并用 streamed 数据填充缓存避免水合闪烁。

本题考察对"数据层与渲染层协作"的理解:Query 负责缓存与预取,Suspense/Streaming 负责渲染编排。答出"服务端 prefetch + 流式补发 + 客户端缓存复用"的完整链路,以及 useSuspenseQuery 相比 useQuery 的差异,即为满分回答。

import { useSuspenseQuery } from "@tanstack/react-query";
function Dashboard() {
  const { data } = useSuspenseQuery({ queryKey: ["stats"], queryFn: fetchStats });
  return <StatBoard data={data} />; // 无需 isPending 分支,Suspense 接管挂起
}
#
★★★

2. Query 请求去重与取消,同一 key 并发请求合并、AbortController 竞态处理与页面卸载取消

TanStack Query 中同一 key 的并发请求如何合并去重?AbortController 如何用于竞态处理与页面卸载取消?

  • 同一 queryKey 并发请求的合并(deduplication)机制
  • signal 传递与 AbortController 的取消链路
  • 页面卸载与组件卸载时的请求取消实践

去重机制:Query 以 queryKey 为唯一标识,同一 key 的并发订阅共享同一查询实例——第一个发起者触发 queryFn,后续订阅者只复用进行中的 Promise 而不会重复发请求,卸载组件也不会取消仍被其他订阅者使用的查询。取消机制:Query v5 把 AbortSignal 传给 queryFn(第三个参数 signal),fetch/axios 收到 signal 即中断底层请求;竞态处理上,旧请求的结果到达时若查询已更新会直接丢弃,配合 signal 可在发起新查询时取消旧请求;页面卸载用 visibilitychange/pagehide 或路由守卫调用 queryClient.cancelQueries 批量中断,避免切换页面后网络连接与回调继续占用资源。工程上应让所有 queryFn 都消费 signal,这是取消链路生效的前提。

答题分两层:去重是"同 key 共享实例"的缓存层行为,取消是"signal 贯穿 queryFn"的网络层行为。能讲清"合并避免重复请求、signal 中断网络、卸载批量取消"三个动作,并指出 queryFn 必须消费 signal 这一前提,即得高分。

const query = useQuery({
  queryKey: ["user", id],
  queryFn: ({ signal }) => fetch(`/api/user/${id}`, { signal }),
});
// 路由离开时批量取消
queryClient.cancelQueries({ queryKey: ["user"] });
#
★★

3. TanStack Router 的 Type-Safe Routing,完全类型安全的参数、loader、search params 如何成为新项目默认?

TanStack Router 的 Type-Safe Routing 如何实现参数、loader 与 search params 的完全类型安全?它为何能成为新项目默认选择?

  • routeTree.gen.ts 代码生成构建类型安全的路由表
  • 路径参数、search params、loader 数据的类型推导链路
  • 与运行时校验(Zod)结合的类型安全边界

类型安全来自代码生成:TanStack Router 根据文件路由扫描生成 routeTree.gen.ts,把每个路由的路径参数、search 类型、loader 返回值与上下文全部推断为 TypeScript 类型,从路由定义到 Link 组件、useParams、useSearch、useLoaderData 全链路编译期校验——参数名拼错、search 字段写错、loader 数据缺字段都会在编译时报错。与 Next.js App Router 相比,search params 也完全类型化(无需手动解析)。实践中配合 Zod 在边界处运行时校验(search 校验失败可跳错误页),实现"编译期类型 + 运行时兜底"双层安全。它成为新项目默认的原因:路由是应用骨架,类型安全让重构、跳转与数据流变更的回归成本大幅下降,且生成文件让 AI 补全与 IDE 提示同样受益。

答题主线是"代码生成为类型安全奠基":routeTree.gen.ts 把路由事实转成可推导类型,参数/loader/search 全部进入类型系统。对比 Next.js 讲清 search params 类型化的差异,再补上 Zod 运行时校验的边界,回答即完整。

const route = createFileRoute("/posts/$postId")({
  loader: ({ params }) => fetchPost(params.postId),
});
// Link 与 useLoaderData 的参数、返回值全部由 routeTree.gen.ts 推导
#
★★

4. TanStack Router 的 useLoaderData 与 TanStack Query 都能取数缓存,二者分工边界(路由预取 vs 组件数据)如何划分?

TanStack Router 的 useLoaderData 与 TanStack Query 都能取数并缓存,路由预取与组件数据的职责边界应如何划分?

  • loader 预取面向"路由级数据"的场景定位
  • Query 面向组件级、可复用、可失效的数据管理
  • 混合使用时的缓存去重与职责划分原则

分工原则是"路由要什么,loader 给什么;组件要什么,Query 管什么":loader 负责路由级数据——决定页面能否渲染的骨架数据(权限、页面配置、关键详情),在导航期间预取并随路由上下文传递,保证进入页面即就绪、无加载态;Query 负责组件级数据——可复用、需频繁刷新与失效的数据(列表、详情、实时状态),提供缓存、重试、失效与乐观更新能力。二者也可配合:loader 中 prefetchQuery 预热 Query 缓存,useLoaderData 负责路由上下文,组件内 Query 做后续刷新与失效,数据不重复请求(同一 key 共享缓存)。划分边界的关键是问"这份数据离开本路由还有价值吗":有则 Query,无则 loader。

本题考"两条取数路径的定位":loader 是路由生命周期的一部分、数据随路由走;Query 是组件生命周期的一部分、数据可复用可失效。能给出"骨架数据走 loader、可复用数据走 Query、loader 可预热 Query 缓存"的清晰边界即为优秀回答。

#
★★

5. TanStack Query v5 的 staleTime 与 gcTime 语义差异,缓存失效与乐观更新如何设计?

TanStack Query v5 中 staleTime 与 gcTime 的语义差异是什么?缓存失效与乐观更新应如何设计?

  • staleTime(数据新鲜期)与 gcTime(缓存驻留期)的区别
  • 自动失效、手动 invalidate 与 refetch 的触发条件
  • 乐观更新(onMutate/onError/onSettled)的流程设计

语义差异:staleTime 是数据被认为"新鲜"的时长,新鲜期内相同 key 的读取不重新请求,过期后才触发后台重取;gcTime 是缓存数据从"不再被订阅"到被垃圾回收清除的驻留时长,控制内存占用。v5 中 gcTime 默认 5 分钟、staleTime 默认 0(数据立即过期),两者正交:staleTime 管"要不要重新请求",gcTime 管"缓存何时释放"。失效设计:写操作成功后调用 invalidateQueries 使相关 key 过期并重取,或用 setQueryData 直接写缓存避免闪烁;乐观更新流程是 onMutate 里取消在途请求、用 setQueryData 写入临时值并保存回滚快照,onError 回滚快照,onSettled 里 invalidate 与服务器对齐。设计原则是"乐观值只做瞬时展示,最终以服务器响应或失效重取为准"。

本题两个考点分开答:先讲清 staleTime 与 gcTime 的"新鲜期 vs 驻留期"正交关系,再讲乐观更新的三段式流程(onMutate 备份与临时写入、onError 回滚、onSettled 对齐)。答出 v5 默认值与"以服务器为准"的兜底原则即完整。

useMutation({
  mutationFn: updateTodo,
  onMutate: async (vars) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] });
    const prev = queryClient.getQueryData(["todos"]);
    queryClient.setQueryData(["todos"], (old) => applyOptimistic(old, vars));
    return { prev }; // 回滚快照
  },
  onError: (_e, _v, ctx) => queryClient.setQueryData(["todos"], ctx.prev),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});
#
★★

6. TanStack Table v8 的 Headless 模式与百万行数据的虚拟滚动工程实践

TanStack Table v8 的 Headless 模式如何工作?面对百万行数据,虚拟滚动的工程实践要点是什么?

  • Headless 模式"逻辑与 UI 分离"的架构设计
  • 虚拟滚动原理(窗口渲染、row virtualization)
  • 大数据量下的性能瓶颈与工程治理手段

Headless 模式指 TanStack Table 只管理表格逻辑(排序、过滤、分页、行选择、列配置),返回 state 与 handler,渲染层完全由开发者用任何 UI 库实现,因此与框架、样式库解耦,逻辑可 100% 测试。百万行数据的核心手段是虚拟滚动:只渲染视口内的行(加 overscan 缓冲),配合固定行高(或测量机制)计算偏移量,DOM 节点数从百万降到几十;表头应 sticky 固定,避免横向滚动时错位;数据侧配合服务端分页/过滤避免一次性传输,纯前端场景则用不可变数据与 memo 化单元格减少重渲染。实践要点还包括:关闭不必要的列内计算、用 row virtualization 而非整表渲染、合并单元格操作在虚拟化下的适配(组头随滚动同步)。Headless + 虚拟滚动让表格在数据量级变化时仍保持可交互与稳定帧率。

答题分两段:Headless 讲"逻辑与渲染分离"的可移植与可测试价值;虚拟滚动讲"只渲染视口 + overscan + 固定行高 + 表头固定"的原理与配套优化。能点出虚拟滚动解决的是 DOM 数量而非数据量问题,即抓住了本质。

const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel() });
const { rows } = table.getRowModel();
// 渲染时结合 @tanstack/react-virtual 只渲染视口内的 rowVirtualizer.getVirtualItems()
#
★★

7. TanStack Start 的全栈能力(loader/actions/server functions)与传统 Next.js 的对比与迁移成本?

TanStack Start 的 loader、actions 与 server functions 等全栈能力与传统 Next.js 相比有何异同?迁移成本如何评估?

  • TanStack Start 与 Next.js 在数据变更(loader/actions)与 server functions 上的模型对比
  • 类型安全、构建器与框架耦合度的差异
  • 从 Next.js 迁移的代码量与心智成本评估

模型对比:TanStack Start 采用与 Router 一致的 loader 模型(路由级预取数据),actions 处理表单提交与服务端变更,server functions 用 createServerFn 定义可被客户端直接调用的服务端函数,所有数据流全程类型安全且基于标准 fetch;Next.js App Router 用 RSC、Server Actions 与 route handlers,运行时更重、约定更多,缓存策略(fetch 缓存、ISR、动态渲染)更复杂。差异要点:TanStack Start 基于 Vite 与标准 Web 标准、框架心智统一(Router+Query 同一作者同一数据哲学)、默认 TanStack Query 集成;Next.js 生态更成熟、中间件与 Image 等内置能力更全。迁移成本评估要看三个维度:数据层(RSC/Server Actions 需改写为 loader/actions/server functions,若项目大量使用 RSC 则成本高)、路由层(App Router 目录约定需映射为文件路由)、增量迁移(TanStack Start 支持渐进式引入,可先在子路由使用)。仅使用静态站点或轻数据场景,迁移性价比低。

本题是框架选型判断题:先讲清两种模型的差异(loader/actions/server functions vs RSC/Server Actions),再按"数据层、路由层、增量路径"评估迁移成本。能给出"什么场景值得迁、什么场景不值得"的结论而非罗列功能,是得分关键。

#
★★

8. TanStack Query 的缓存与失效,staleTime/refetch 策略?

TanStack Query 的缓存与失效策略应如何设计?staleTime 与 refetch 机制如何配合?

  • 缓存命中的判定(key + stale 状态)与默认 refetch 触发场景
  • staleTime/refetchOnWindowFocus/refetchInterval 的策略组合
  • 写后失效(invalidate)与读取的联动设计

策略设计围绕"数据新鲜度"展开:staleTime 定义新鲜期,新鲜期内任何读取直接命中缓存不发请求;数据过期后,refetch 的触发点包括窗口聚焦(refetchOnWindowFocus)、组件挂载(refetchOnMount)、网络重连(refetchOnReconnect)与手动 refetch。合理策略是按数据特性分类:低频静态数据(配置、字典)设较长 staleTime 甚至 staleTime: Infinity 只在失效时更新;高频动态数据(行情、在线状态)配合 refetchInterval 轮询;表单页避免后台刷新干扰输入,可关闭窗口聚焦重取。失效链路:写操作成功后 invalidateQueries 使相关 key 过期并立即重取,或 setQueryData 乐观写入后后台对齐;多 key 联动时用谓词(queryKey 前缀匹配)批量失效。设计原则是"读靠新鲜期挡请求、写靠失效保一致、轮询只给真需要的数据"。

本题考察"按数据特性配置"的策略思维:不要默认配置,而是区分静态/动态/交互型数据分别定 staleTime 与 refetch 触发点。答出"新鲜期挡请求 + invalidate 保证写后一致 + 轮询仅限实时数据"的框架即完整。

#
★★

9. TanStack Query 的乐观更新与回滚?

TanStack Query 的乐观更新与回滚机制是如何工作的?有哪些设计要点?

  • 乐观更新的三段式生命周期(onMutate/onError/onSettled)
  • 回滚快照的保存与恢复时机
  • 取消在途请求与最终对齐的策略

乐观更新流程:onMutate 在请求发出前先用 setQueryData 把预期结果写入缓存,用户立即看到新状态;同时 cancelQueries 取消该 key 的在途请求,防止旧响应覆盖乐观值,并保存旧数据快照作为回滚依据;请求失败时 onError 用快照恢复缓存并提示错误;无论成败,onSettled 都 invalidateQueries 让缓存与服务器最终对齐。设计要点:快照必须与变更的 key 精确对应(多 key 变更要逐 key 保存);乐观值应只写"本次变更影响的数据",避免用不完整数据覆盖整份缓存(可用 updater 函数局部修改);对并发修改要谨慎,多客户端场景更依赖失效重取而非乐观写入。回滚的兜底原则是"乐观是体验优化,失效重取才是一致性保证"。

答题主干是"三段式 + 快照 + 对齐":onMutate 写乐观值与快照、onError 回滚、onSettled 失效对齐,并强调 cancelQueries 防止竞态覆盖。能点出"乐观更新不改变最终一致性,仅优化感知"即理解到位。

#
★★

10. TanStack Query 的持久化缓存(persistQueryClient),异步 persister、shouldDehydrateQuery 过滤与缓存版本迁移在离线场景的工程价值?

TanStack Query 的持久化缓存(persistQueryClient)如何工作?异步 persister、shouldDehydrateQuery 过滤与缓存版本迁移在离线场景有何工程价值?

  • persistQueryClient 与 persister 接口(createSyncStoragePersister/createAsyncStoragePersister)
  • shouldDehydrateQuery 的过滤策略与数据安全
  • 缓存版本(version)管理与迁移机制

persistQueryClient 把查询缓存持久化到 localStorage/IndexedDB,刷新或离线重开时先以缓存渲染再后台重取,秒开体验的关键。异步 persister(createAsyncStoragePersister 基于 IndexedDB)避免同步 localStorage 大缓存阻塞主线程,支持大数据量且容量更高。shouldDehydrateQuery 是持久化的白名单过滤器:默认不持久化 Infinity 缓存,实践中用它排除含敏感数据、体积巨大或实时性要求高的查询,只持久化低频可复用的数据,控制写入成本与隐私风险。缓存版本迁移:persister 存 buster 标识,Query 更新后 bump 版本(如从 1 到 2),读取时版本不匹配即整体丢弃旧缓存,防止旧结构数据喂给新代码导致崩溃。离线场景的价值链是"持久化命中秒开 → 过滤保证安全与体积 → 版本管理保证结构演进安全"。

本题三个要点对应三个机制:异步 persister 管"怎么存"(不阻塞主线程、容量大)、shouldDehydrateQuery 管"存什么"(敏感与大体积数据排除)、版本迁移管"存旧了怎么办"(不兼容即废弃)。三者合起来才是离线缓存工程的完整答案。

const persister = createAsyncStoragePersister({
  storage: createIndexedDbStorage({ dbName: "qcache" }),
});
persistQueryClient({
  queryClient,
  persister,
  buster: "v2",
  dehydrateOptions: {
    shouldDehydrateQuery: ({ queryKey, query }) =>
      !queryKey.includes("sensitive") && query.state.data !== undefined,
  },
});
#
★★

11. v5 structural sharing,查询数据引用稳定性的渲染优化价值与需要关闭的场景

TanStack Query v5 的 structural sharing 机制如何工作?它对渲染优化有何价值,又有哪些需要关闭的场景?

  • structural sharing 的引用复用原理(递归比较、保留未变引用)
  • 对 React 重渲染与 memo 化的收益
  • 需要关闭的场景(不可变数据、含 class 实例/函数等非纯 JSON 数据)

structural sharing 是 Query 更新缓存时的一项优化:新数据与旧数据递归比较,未变化的分支复用旧引用而非整体替换,只有变化的分支生成新对象。价值在于查询数据被组件消费时,未变的字段引用稳定,React 的 memo 化组件与浅比较(React.memo、useMemo 依赖、zustand 选择器等)能跳过大量重渲染——尤其列表数据后台刷新时,未变行对象的引用不变,行组件不会重渲染。需要关闭的场景:数据含非纯 JSON 结构(Date 实例、Map、class 实例、函数)时递归比较会破坏原型链或产生误判;数据量大且每次几乎全变时比较开销反超收益;需要强制新引用触发渲染的罕见场景。v5 中可用 structuralSharing: false 关闭。取舍原则是"默认开启收益大,遇到非纯数据结构或比较开销过高时按查询关闭"。

答题要点是"引用稳定性"这个核心:structural sharing 不改变数据内容,只让未变部分的引用保持不变,从而让浅比较与 memo 化生效。答出关闭场景(非纯 JSON、全量变化大数据)体现工程经验。

#
★★

12. enabled 控制的串行依赖查询与 placeholderData 占位数据衔接,避免加载态闪烁

如何用 enabled 控制串行依赖查询?placeholderData 占位数据如何与串行查询衔接以避免加载态闪烁?

  • enabled: false 暂停查询与依赖链的编排
  • placeholderData 的类型与来源(previousData/函数)
  • 串行依赖中加载态与占位数据的过渡设计

串行依赖场景(先取用户,再按其 id 取订单)用 enabled 控制:第二个查询在依赖数据就绪前 enabled: false 处于暂停态,不发起请求也不报错,依赖就绪后自动激活。衔接问题在于暂停→激活的瞬间会短暂进入 pending,用户看到加载态闪烁。解法是 placeholderData:给依赖查询提供占位数据,pending 期间直接渲染占位值不闪烁;工程上常用 placeholderData: (prev) => prev 或 placeholderData: keepPreviousData 保留上次数据,使依赖变化(如翻页、切筛选)时旧数据继续展示直到新数据到达。组合设计:enabled 管"什么时候发请求",placeholderData 管"等待时展示什么",两者配合让串行依赖链路全程无空白闪烁,配合 isPlaceholderData 标志弱化占位数据的视觉权重。

本题考点是"暂停与占位的组合拳":enabled 负责依赖链的编排(不满足条件不发请求),placeholderData 负责等待期的展示(旧数据续显避免闪烁)。答出 isPlaceholderData 的提示语义是加分项。

const userQuery = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const ordersQuery = useQuery({
  queryKey: ["orders", userQuery.data?.id],
  queryFn: () => fetchOrders(userQuery.data.id),
  enabled: !!userQuery.data?.id,
  placeholderData: keepPreviousData, // 依赖切换时旧数据续显
});
#
★★

13. TanStack Start Server Functions 的序列化边界与安全暴露面,不可序列化参数与调用权限控制

TanStack Start Server Functions 的序列化边界是什么?不可序列化参数如何处理?调用权限如何控制?

  • Server Functions 参数/返回值的序列化规则(JSON 兼容数据)
  • 不可序列化数据(File、Date、Map、函数)的处理方式
  • 服务端暴露面的鉴权与防护设计

Server Functions(createServerFn)在客户端调用时通过 HTTP 传递参数与返回值,序列化边界是"必须 JSON 可序列化":普通对象、数组、字符串、数字可直接传;File/Blob 需先转 base64 或走单独上传接口;Date 转时间戳/ISO 字符串;Map/Set、函数、循环引用、含原型方法的数据不可直接传,需显式降级为纯数据结构。返回值同理,服务端返回的数据会序列化传输,含敏感信息要按需裁剪。安全暴露面:serverFn 是公开 HTTP 端点,必须在函数内部做鉴权(校验 session、权限),不能假设只有自己的页面调用;同时要防参数注入,服务端做输入校验(如 zod),对批量接口限流。设计原则是"序列化边界用显式转换管理,安全边界用服务端鉴权兜底"。

本题两个维度:序列化边界讲"什么是 JSON 兼容数据、不可序列化怎么办",安全暴露面讲"公开端点 + 服务端鉴权 + 输入校验"。能点出"serverFn 本质是 HTTP 端点、客户端校验不可信"即为关键得分点。

const submitOrder = createServerFn({ method: "POST" })
  .validator((d: unknown) => orderSchema.parse(d)) // 服务端输入校验
  .handler(async ({ data }) => {
    if (!(await requireUser())) throw new Error("unauthorized"); // 服务端鉴权
    return db.orders.create({ ...data, createdAt: new Date().toISOString() });
  });
#

14. TanStack Form 的类型化表单与 Zod 校验的深度集成

TanStack Form 如何实现类型化表单?它与 Zod 校验的深度集成体现在哪些方面?

  • useForm 的泛型状态管理与字段级类型推导
  • Zod 校验器与 form 状态、错误信息的绑定
  • 受控组件、异步校验与表单提交流程

TanStack Form 的 useForm 用泛型把表单数据结构贯穿整个状态管理:字段值、错误、变更处理全部类型推导,增删字段编译期即报错。与 Zod 集成的方式是 validatorAdapter:把 zodSchema 传给 form 的 validators,字段级 schema 与表单级 schema 分别驱动字段与提交校验,校验结果自动映射为字段错误信息,且遵循"失焦/变更/提交"各时机的校验触发配置。深度集成还体现在:字段 onChange/onBlur 校验与受控组件无缝配合(value/onChange 直接绑定 input)、异步校验(unique 检查)返回 Promise、提交前整体校验并聚焦首个错误字段、状态(isSubmitting/isValid)驱动按钮禁用。相比手写 useState 管理表单,类型与校验的声明式组合让表单逻辑可测、可维护。

答题要点是"泛型贯穿 + validator 声明式接入":类型安全来自 useForm 泛型与 Zod 推导的合一,校验来自 validatorAdapter 将 schema 注入字段与表单两级。能讲清校验时机与提交流程即完整。

const form = useForm({
  defaultValues: { email: "", age: 0 },
  validators: { onChange: zodValidator(userSchema) },
});
#

15. Query 键结构与缓存更新,列表-详情联动失效、无限滚动的缓存管理最佳实践?

TanStack Query 的键结构应如何设计?列表与详情联动失效、无限滚动的缓存管理有哪些最佳实践?

  • queryKey 的分层结构与唯一性设计
  • 列表-详情失效的 key 前缀联动策略
  • 无限滚动(useInfiniteQuery)的缓存更新与失效

键结构设计遵循"分层 + 可预测":第一层是资源名(["todos"]、["users"]),第二层是过滤条件或 id(["todos", { status }]),规律的结构让前缀匹配失效(invalidateQueries({ queryKey: ["todos"] }))能批量命中相关查询。列表-详情联动:详情 key 与列表 key 共享前缀(列表 ["posts"],详情 ["posts", id]),对详情做写操作后按前缀失效即可同时刷新列表项与详情,避免"改了详情列表不更新"。无限滚动的缓存管理:用 useInfiniteQuery 的 getNextPageParam 管理页游标,缓存按页存储;更新某一页数据用 updater 函数在 setQueryData 中按页码局部替换,不整表重建;失效时注意"刷新第一页并重置分页"与"保留已加载页仅追加"两种策略的选择,配合 initialPageParam 与 maxPages 控制内存。最佳实践是"key 前缀即失效域、详情共享列表前缀、无限滚动按页局部更新"。

答题主线是"key 即缓存地址与失效域":前缀设计决定批量失效的能力,详情复用列表前缀实现联动,无限滚动讲清按页存储与局部更新。能答出失效谓词与页级更新细节即达到最佳实践标准。

// 列表与详情共享前缀,写详情后按前缀失效即可联动
queryClient.invalidateQueries({ queryKey: ["posts"] });
// 无限滚动:页级局部更新
queryClient.setQueryData(["posts"], (old) =>
  old ? { ...old, pages: old.pages.map((p, i) => (i === target ? newPage : p)) } : old
);
#

16. TanStack Table 与虚拟滚动,大数据量表格?

大数据量表格中,TanStack Table 与虚拟滚动应如何配合使用?有哪些注意点?

  • 逻辑层(Table)与渲染层(虚拟滚动)的协作方式
  • 行高、测量与滚动偏移的计算
  • 排序/过滤与虚拟化共存时的联动问题

协作方式:TanStack Table 只提供排序、过滤、分组等处理后的行模型(getRowModel),虚拟滚动接管渲染——用 @tanstack/react-virtual 把"全部行"与"视口行"映射,只渲染可见行加 overscan 缓冲。注意点:虚拟滚动依赖稳定行高估算总高,固定行高用 estimateSize 精确计算,动态行高需测量回调动态调整,否则滚动条跳动;表头需 sticky 且与滚动容器同步;排序/过滤作用于逻辑层(行模型变化),虚拟滚动自动跟随,无需干预,但全局搜索过滤后行数骤减需重置滚动位置;合并单元格、行展开等特性需自行处理高度变化。性能上建议行组件 memo 化、单元格避免匿名函数内联、大表关闭不必要的重新渲染订阅。最佳实践是"逻辑交给 Table、渲染交给虚拟滚动、高度交给测量、性能交给 memo"。

本题考察"职责分离":Table 管数据逻辑,虚拟滚动管 DOM 数量,难点在行高测量与 sticky 表头的同步。答出固定行高估算与动态测量的取舍,以及排序过滤在逻辑层自动生效,即为完整回答。

#

17. TanStack 表单与验证,受控与性能?

TanStack Form 的受控表单如何实现?在大表单场景下如何保证验证与渲染性能?

  • 字段级订阅与局部重渲染机制
  • 受控 value/onChange 的绑定与校验时机
  • 大表单的性能优化手段(字段隔离、校验防抖、减少全表渲染)

受控实现:TanStack Form 的每个字段都是独立状态单元,useStore 订阅单字段(form.useStore(s => s.values.fieldA)),字段值变化只触发该字段组件重渲染,而非整个表单——这是它与 useState 整表单管理的关键差异。绑定方式与传统受控一致:input 的 value 取自字段状态,onChange 调用 form.setFieldValue,同时配置校验时机(onChange/onBlur/onSubmit)。大表单性能要点:字段级订阅避免全表重渲染;高频输入(搜索框)校验用防抖;异步校验(唯一性检查)用 debounce 加请求取消避免乱序响应;大数据量字段(富文本、代码编辑)隔离为独立组件并用 memo;提交与状态汇总(isValid)按需订阅,避免每字符触发全表单状态计算。验证与性能的统一原则是"状态粒度越小,重渲染面越小"。

本题核心是"字段级订阅"这一架构差异:TanStack Form 的状态按字段分布、按需订阅,天然规避大表单全量重渲染。答出校验防抖与异步校验取消即覆盖性能要点。

#

18. TanStack 生态的架构,Query/Table/Router 协作?

TanStack 生态中 Query、Table 与 Router 如何协作?它们之间的职责边界是什么?

  • 三者各自的职责定位(数据获取/表格逻辑/路由)
  • 协作点:loader 预取、表格数据驱动、路由参数联动
  • 依赖方向与可替换性设计

职责边界:Router 管导航与路由级数据(loader 预取、参数、search),Query 管组件级数据获取与缓存,Table 管表格交互逻辑(排序、过滤、分页、选择),三者各管一层互不侵入。协作模式:Router 的 loader 里调用 queryClient.prefetchQuery 预热数据,页面进入即用 Query 缓存渲染;search params 承载表格状态(页码、排序、筛选)时,Router 提供类型安全的 search 读写,表格的 state 变化写回 search(onStateChange 与 router.navigate 联动),实现"URL 即状态、可分享可刷新";Query 的查询参数又来自 Router 的路径参数,依赖方向单向(Router → Query → Table 渲染)。可替换性:Query 可用其他数据层替换而不影响 Router,Table 完全独立,生态内部通过"标准接口 + 单一职责"组合,避免紧耦合。整体架构价值是"每一层都可独立测试、独立替换、独立演进"。

本题考察架构分层认知:先讲清三者职责(导航/数据/表格逻辑),再讲协作点(loader 预热、search 承载表格状态),最后强调依赖单向与可替换性。能画出"URL 状态 → Query 数据 → Table 展示"的数据流即到位。

#

19. TanStack Query 的服务端缓存与 SSR?

TanStack Query 在服务端渲染(SSR)场景下如何工作?服务端缓存与客户端缓存如何衔接?

  • SSR 下 prefetchQuery 与 dehydrate/hydrate 的流程
  • 服务端与客户端缓存的衔接(序列化传递、避免重复请求)
  • SSR 场景的缓存隔离与 staleTime 配置

SSR 流程分四步:服务端创建独立 queryClient(每个请求一个实例,避免请求间缓存串扰),用 prefetchQuery 预取页面所需数据,渲染完成后用 dehydrate 把查询缓存序列化为 JSON 注入 HTML,客户端用 hydrate 恢复缓存后渲染。衔接要点:hydrate 后的缓存数据被标记为 fresh(dehydrate 的缓存默认作为新鲜数据处理),首屏直接使用服务端数据不发二次请求;客户端 queryClient 复用被 hydrate 的缓存,后续失效与重取正常进行。配置注意:服务端实例的 staleTime 建议设置较大值避免重复预取,gcTime 在服务端不适用(请求即销毁);流式 SSR 场景用 prefetchInfiniteQuery 预取分页数据、配合 Suspense 逐段交付。隔离原则是"服务端每请求一实例、客户端单例,交界处用 dehydrate/hydrate 传递"。

答题主干是"独立实例 + 预取 + 脱水/注水":服务端每请求新建实例防串扰,dehydrate 序列化缓存,hydrate 恢复且按新鲜数据处理避免二次请求。能讲出服务端实例生命周期与 staleTime 差异即为完整。

// 服务端
const queryClient = new QueryClient(); // 每请求一实例
await queryClient.prefetchQuery({ queryKey: ["home"], queryFn: fetchHome });
const dehydrated = dehydrate(queryClient); // 注入 HTML
// 客户端
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } });
hydrate(queryClient, dehydrated);
#

20. TanStack Router 的基于文件的路由(file-based routing)与 routeTree.gen.ts 代码生成在类型安全路由的工程实践?

TanStack Router 基于文件的路由与 routeTree.gen.ts 代码生成如何实践?它们如何支撑类型安全路由?

  • 文件路由的目录约定($param、layout、pathless route)
  • routeTree.gen.ts 的生成时机与内容
  • 生成文件在类型安全与重构中的工程作用

文件路由约定:routes 目录下文件即路由,文件名用 $param 表示动态段,@layout 前缀表示布局路由,_pathless 表示无路径布局,(group) 表示路由分组,命名约定直接映射路径结构。routeTree.gen.ts 是构建/开发时由插件扫描文件路由自动生成的类型清单:它把每个路由的路径、参数、search、loader 返回类型与上下文合并进一棵类型安全的路由树,Link、useNavigate、useParams、useLoaderData 全部从此推导类型。工程实践:生成文件纳入版本控制(团队共享同一类型基线)、路由改动后无需手改类型(重新生成即可)、配合 IDE 插件(TanStack Router Devtools)实时更新;重构时改文件路由名,引用处类型错误即时暴露。价值在于"路由结构即类型事实",跳转与取数不可能写出运行时才发现的错误。

本题要点是"约定即类型":文件命名约定定义路由结构,routeTree.gen.ts 把结构转成可推导类型,类型错误在编译期暴露。答出生成文件的版本控制与重新生成机制即为工程实践到位。

#

21. TanStack Router 路由级代码分割(route.lazy)与导航 pending 状态最小化

TanStack Router 的路由级代码分割(route.lazy)如何实现?如何最小化导航 pending 状态?

  • route.lazy 的按需加载机制与触发时机
  • 加载期间 pending 状态的呈现策略
  • 预取(prefetch)与缓存对导航体验的优化

route.lazy 把路由组件与 loader 拆分为独立 chunk,用户导航到该路由时才加载,显著减小首屏包体;TanStack Router 的 lazy 在路由匹配时自动触发加载,加载完成前可用 pendingComponent 显示骨架屏或保持旧页面。最小化 pending 状态的手段:一是预取,Link 组件 hover/聚焦时自动 prefetch(preload 策略),或导航前 prefetchRoute 预热 chunk 与数据,让"点击即已就绪";二是 loader 与组件并行加载,避免串行等待;三是用中间状态分层呈现——路由切换时先显示旧页(pending 期间不闪白),数据到达后过渡到新页;四是配合 startTransition 与 View Transitions 做平滑切换。工程原则是"包体靠 lazy 拆分、等待靠预取消除、剩余等待靠骨架屏承接"。

本题两个动作:lazy 解决"体积"(按需拆包),预取与中间态解决"等待"(点击前预热、等待时承接)。能答出 Link 的 preload 与 pendingComponent 即证明有实操。

const route = createFileRoute("/admin")({
  lazyRouteComponent: () => import("./admin.lazy").then((m) => m.Route),
  pendingComponent: AdminSkeleton, // 加载期间显示骨架屏
});