关于 tanstack-data-loading
tanstack-data-loading 是面向 TanStack React Start 项目的编码技能,指导编写和审查数据加载代码。它通过项目发现、版本检查及按后端类型选择规则,聚焦加载器、SSR、组件和变更流程。
主要功能
- 01按模式执行编写、审计或刷新
- 02发现应用配置并缓存结果
- 03检查 TanStack 依赖版本
- 04指导加载器并发与错误处理
- 05涵盖 SSR、组件和变更规则
适用场景
- 01检查路由加载器中的请求瀑布
- 02为应用刷新数据加载发现记录
- 03编写符合项目后端结构的服务函数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ draekien/ skills --skill tanstack-data-loading安装后在 Claude Code 中输入/tanstack-data-loading确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/audit.mdbackend-database.mdbackend-service.mdversions.mdscripts/discovery.py详细介绍
TanStack 数据加载
第一个参数用于选择模式。如果未指定模式,则根据请求推断:请求审计、查找瀑布问题或检查现有代码时,使用 audit;请求重新发现或清除缓存时,使用 refresh;其他情况则使用编写模式。
audit [--fix] [path]— 开始前先阅读 references/audit.md,并对path执行审计。refresh [path]— 对包含path的应用重新运行 Discovery,写入结果,然后停止。- 编写模式 — 按照以下规则处理正在编写或审查的代码;如果指定了
path,则仅在该路径范围内处理。
每种模式都从 Discovery 开始。规则取决于 Discovery 记录的内容:如果你猜测后端结构,就会套用错误的服务器函数规则和错误的编写规则。
可用脚本
scripts/discovery.py—read和write位于<repo-root>/.draekien/tanstack-data-loading/discovery.json的 Discovery 缓存,每个应用根目录对应一条 Discovery 记录;versions会显示已安装的@tanstack版本。退出代码请查看--help。
发现
应用根目录。 path(或当前目录)的最近一个祖先目录,其 package.json 依赖于 @tanstack/react-start。如果没有匹配项,或任务涉及多个应用,请询问用户要处理哪个应用。
读取。 在此技能的目录中运行脚本,并传入相对于仓库根目录的应用根目录(单包仓库使用 .):
uv run scripts/discovery.py read --app apps/web --repo <repo-root>
| 退出码 | 含义 | 下一步 |
|---|---|---|
| 0 | 已打印最新的发现条目 | 使用该条目,并将其 versionNotes 应用于对应规则。在 refresh 中,无论如何都要执行发现并写入。 |
| 4 | 没有发现条目 | 执行发现并写入。 |
| 5 | 条目早于脚本允许的最大时限,或已安装的 @tanstack 版本发生变化 | 执行发现并写入。 |
| 3 | 缓存文件或已安装的包清单无法读取 | 查看标准错误输出。对于缓存文件,告知用户,并且只有在用户同意后才删除,因为其中保存了所有应用的发现条目。对于包清单,请用户重新安装依赖项。 |
| 2 | 用法错误 | 查看标准错误输出,修正调用后重新运行。 |
发现。 从代码中找出各字段:
| 字段 | 值 | 查找方式 |
|---|---|---|
backend | database、service、both | 阅读 createServerFn 处理程序。database:直接查询数据库客户端。service:通过 HTTP 调用其他服务。both:两种情况都有。 |
pendingUi | route-level、per-section | 路由器设置了 defaultPendingComponent 时使用 route-level。否则,询问用户想要路由级待处理组件还是仅使用 Suspense 分区;如果用户不可联系,则记录 per-section 并说明这一点。 |
verify | 命令列表 | 从 package.json 的 scripts 中查找仓库的 lint、类型检查和测试命令。 |
versionNotes | 字符串列表 | 运行 uv run scripts/discovery.py versions --app <app-root> --repo <repo-root>,并与 版本 对照。如果该命令引导你查看 references/versions.md,则记录其发现结果;如果没有发现,则记录 checked: no changes。否则留空。如果 versions 输出 {},请用户先安装依赖项再继续。 |
写入。 脚本会添加 discoveredAt 和已安装的 versions,并拒绝未知值:
uv run scripts/discovery.py write --app apps/web --repo <repo-root> <<'EOF'
{"backend": "database", "pendingUi": "per-section", "verify": ["pnpm check", "pnpm typecheck", "pnpm test"], "versionNotes": []}
EOF
在仓库中首次写入。 退出码 6 表示缺少 .draekien/:创建之前请先询问用户。如果 .draekien/tanstack-data-loading/discovery.json 尚不存在,请询问是否将 .draekien/tanstack-data-loading/ 添加到 .gitignore;发现条目仅保存一位开发者的本地发现信息。如果用户拒绝创建 .draekien/,则仅在本次任务中保留已发现的字段,并说明未写入发现缓存。
对于该应用,read 或 write 以退出码 0 结束,或者用户拒绝缓存且所有字段均已确定时,发现即告完成。
后端规则。 在编写或审查任何服务端函数、变更或写入路径之前,请先阅读与 backend 对应的参考文档:
database— references/backend-database.md。service— references/backend-service.md。both— 两份都要阅读。分别将其应用于处理程序符合对应情况的服务端函数;同时执行两种操作的处理程序,其不同部分分别应用对应的参考文档。
版本
规则已针对 @tanstack/react-query 5.104.2、@tanstack/react-router 1.170.44 和 @tanstack/react-start 1.169.1 进行检查。应用使用以下 API 的规则之前,请将已安装版本(发现条目中的 versions,或 versions 命令)与最低版本进行比较;如果已安装版本较低,请使用替代方案:
| 规则使用 | 需要 | 低于最低版本时 |
|---|---|---|
queryClient.query, queryClient.infiniteQuery | @tanstack/react-query 5.102.0 | fetchQuery、prefetchQuery、ensureQueryData 及其无限查询变体,这些 API 在该版本中未弃用 |
staleTime: "static" | @tanstack/react-query 5.79.0 | ensureQueryData(options),它在 loader 中的行为相同 |
import { noop } from "@tanstack/react-query" | @tanstack/react-query 5.76.0 | .catch(() => {}) |
mutation scope: { id } | @tanstack/react-query 5.32.0 | 在 isPending 期间禁用触发器 |
createServerFn().validator(...) | @tanstack/react-start 1.168.25 | .inputValidator(...);1.169.0 及更高版本仅接受 .validator(...) |
禁止从客户端导入 *.server.ts | @tanstack/react-start 1.166.8 | 该后缀只是命名约定;不会阻止导入 |
如果已安装的主版本高于经过检查的版本,或者规则使用的 API 在已安装的类型中不存在,并且发现条目没有 versionNotes,请先阅读 references/versions.md,再应用任何规则。
加载器
适用范围:应用根目录下所有文件路由的 loader 选项。
如果路由缺少某项读取就无法渲染,或者读取失败时必须渲染其他内容,那么这项读取就是关键读取:例如主要内容、是否存在(404)、授权(403)、重定向、限制门槛、表单默认值。其他所有读取都是非等待读取。
-
关键读取:
await context.queryClient.query(options)。在加载器中等待它,会在渲染前将 404 或 403 发送到notFoundComponent或errorComponent;让错误传递给它们,或抛出notFound()。 -
非等待读取,包括切换项的第二个时间段等隐藏变体:
void context.queryClient.query(options).catch(noop)。如果没有.catch(noop),拒绝会在服务端未处理;组件的useSuspenseQuery仍会将错误重新抛给其边界。 -
在第一个
await之前启动所有非等待读取。在await之后启动的读取会形成瀑布流。 -
并行启动相互独立的关键读取:一个
await Promise.all([...])。✗ await context.queryClient.query(orderQueryOptions(id)) await context.queryClient.query(customerQueryOptions(customerId)) ✓ await Promise.all([ context.queryClient.query(orderQueryOptions(id)), context.queryClient.query(customerQueryOptions(customerId)), ]) -
如果读取
b需要读取a的结果,就会形成无法消除的瀑布流:ab加载器 关键 关键 const x = await query(a),然后await query(b(x))关键 非等待 const x = await query(a),然后void query(b(x)).catch(noop):这是在await之后启动的唯一一次非等待读取非等待 非等待 void query(a).then((x) => query(b(x))).catch(noop),在第一个await之前启动 -
加载器和组件使用同一个
queryOptions对象。组件中使用不同的键会再次获取数据。 -
已登出的访客可以打开的路由,只能在已登录分支中启动需要会话的读取。否则,每次已登出访客访问都会触发报错的调用。
-
替换已弃用的
QueryClient方法:已弃用 替代方式 fetchQuery(options)query(options)prefetchQuery(options)void query(options).catch(noop)ensureQueryData(options)query({ ...options, staleTime: "static" }),行为相同。如果某项变更会使该键失效,则改用staleTime: Infinity:之后运行的加载器使用"static"会返回已失效的缓存数据,因此路由会根据旧数据决定分支、重定向或门槛;而Infinity会先重新获取数据。传给单次query()调用的staleTime不会保留在查询上:失效后是否重新获取已挂载页面的数据,取决于组件useSuspenseQuery的staleTimefetchInfiniteQuery(options)infiniteQuery(options)prefetchInfiniteQuery(options)void infiniteQuery(options).catch(noop)ensureInfiniteQueryData(options)infiniteQuery(...),使用与ensureQueryData相同的staleTime选择 -
不要将读取推迟到滚动或交互时,也不要在路由器的
defaultPreload: "intent"之外添加悬停预加载。非等待读取已经会以流式方式加载,不会阻塞页面;推迟读取会在渲染后增加一次请求,而且 TanStack 文档没有说明推迟读取与useSuspenseQuery配合时的行为。
流式传输和 SSR
范围:路由器文件和每个路由的 ssr 选项。
- 这里的规则仅在路由器文件调用
@tanstack/react-router-ssr-query中的setupRouterSsrQueryIntegration时适用。如果没有调用,请将此项作为第一条发现报告。 - 未等待的读取会在服务器上运行,并流式传输到客户端,不会阻塞服务器响应。关键读取会阻塞响应,直到读取完成。
useSuspenseQuery和 loader 读取会在服务器上运行。useQuery不会;它的数据会在 hydration 后加载,因此,使用useQuery读取数据的路由组件会形成瀑布。ssr: "data-only"会在服务器上运行beforeLoad和loader,并仅在客户端渲染组件。不要依赖ssr: "data-only"下未等待的读取会进行流式传输,也不要将其作为解决方案推荐;没有官方文档说明这种行为。如果任务依赖此行为,请在运行中的应用里验证。- 不要配置路由代码拆分:Start 始终启用代码拆分,且 loader 会保留在主代码块中。
组件
范围:使用 suspense 读取查询数据的组件。
-
一个组件中有两个或更多 suspense 查询时:使用一次
useSuspenseQueries调用。在同一组件中分别调用useSuspenseQuery会形成瀑布:第二个查询要等第一个完成后才会开始。✗ const { data: order } = useSuspenseQuery(orderQueryOptions(id)) const { data: notes } = useSuspenseQuery(notesQueryOptions(id)) ✓ const [{ data: order }, { data: notes }] = useSuspenseQueries({ queries: [orderQueryOptions(id), notesQueryOptions(id)], }) -
如果子组件会因路由本可以预先知道的查询而暂停,请在路由 loader 中启动该读取。否则,只有父组件渲染后,子组件的请求才会开始。
-
每个未等待的读取都应在自己的 Suspense 区块中渲染:在
QueryErrorResetBoundary内,在<React.Suspense fallback={...}>外围使用来自@tanstack/react-router的<CatchBoundary getResetKey={...} errorComponent={...}>,并在错误界面提供重试。该区块流式传输时,页面其余部分仍会渲染;错误也会限制在该区块内。关键读取无需 Suspense 区块。 -
如果查询键会在同一路由上发生变化(搜索参数、标签页、页码、周期),请在
React.startTransition中执行此变更。useSuspenseQuery没有placeholderData;如果不使用 transition,React 会将内容替换为 fallback。 -
pendingUi: route-level:路由器会设置defaultPendingComponent,并在应用的 shell 布局内渲染。经过defaultPendingMs(默认值为 1000)后开始渲染,且至少显示defaultPendingMinMs(默认值为 500)。pendingUi: per-section:不要添加defaultPendingComponent;如果用户选择了此模式,而路由器设置了该组件,则保留它,直到用户要求移除。
变更
范围:每个 useMutation。
-
服务端函数返回已保存的实体。
onSuccess使用queryClient.setQueryData(key, saved)写入该实体。 -
onSuccess仅使由此次变更推导出的键(列表、计数、总计)失效,且不等待失效操作。如果在onSuccess中返回或等待一个 promise,isPending会一直为 true,直到所有失效的查询都完成重新获取。✗ onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orderKeys.all }) } ✓ onSuccess: (saved) => { queryClient.setQueryData(orderKeys.detail(saved.id), saved) void queryClient.invalidateQueries({ queryKey: orderKeys.lists() }) } -
切勿使刚写入的键失效;失效会触发该键重新获取。要使包含该键的前缀失效,请使用
predicate跳过已写入的键:void queryClient.invalidateQueries({ queryKey: orderKeys.all, predicate: (query) => query.queryKey.length !== written.length || query.queryKey.some((part, index) => part !== written[index]), }) -
对同一实体进行可能重叠的保存(自动保存、快速切换)时,请将
scope: { id }设置为一个用于命名该实体的字符串,例如`order-${orderId}`。具有相同scope.id的变更会按调用顺序逐个运行。每次保存后重新获取无法解决顺序问题:响应仍可能乱序到达。 -
如果某项变更会改变用户可查看的数据(切换账户或工作区),则等待其失效操作完成,以确保下一屏不会使用之前账户的数据进行渲染。其他所有变更均遵循上述规则。
服务端函数
范围:所有 createServerFn。
- 将
createServerFn导出放在*.functions.ts中。客户端代码可以导入它们;构建过程会将每个处理函数替换为请求存根。仅限服务端使用的辅助函数放在*.server.ts中,导入保护机制会阻止客户端代码导入这些文件。 - 验证输入:
.validator(z.object({ ... }))。 - 客户端的每次调用都会发送一个 HTTP 请求,成本固定:请求本身,以及它运行的每个中间件。不要批处理或对调用去重:对三个服务端函数使用
Promise.all会发送三个并发请求,两次相同的调用也会发送两次请求。通过只在queryFn中调用服务端函数,利用查询缓存对相同的读取操作去重。 - 不要把页面上的所有读取操作合并到一个服务端函数中。这样页面就必须等待最慢的部分,Suspense 区块无法分别流式传输,而且每次变更都会重新获取所有数据。
- 中间件会在使用它的每个服务端函数的每次调用中运行。共享中间件只计算所有这些函数都会读取的内容;只有部分函数读取的值应放在各自的中间件中。
- 在同一个数据库连接上发送的语句(包括单个事务中的所有语句)会逐条运行。对这些语句使用
Promise.all不会节省时间;应减少语句数量。 - 将 Discovery 下名为“Backend rules”的后端参考应用于每个服务端函数。
注意事项
useSuspenseQueries不接受无限查询。同一组件中的无限查询仍会在其他查询解析后才开始;应在加载器中启动它。- 转换为
useSuspenseQueries后,所有对@tanstack/react-query的测试模拟都必须导出useSuspenseQueries。 - 对缓存行使用
toBe会失败:结构共享会返回一个相等的副本,而不是同一个对象。请使用toEqual。 - 加载器一旦等待某次读取,如果写入后该读取立即返回 404(后端异步更新其读取模型),路由就会显示错误组件,而不是在 Suspense 区块内显示错误。