mcpField
T
tanstack-data-loading

tanstack-data-loading

github.com/draekien开发158 次安装2 周前更新
代码仓库

关于 tanstack-data-loading

tanstack-data-loading 是面向 TanStack React Start 项目的编码技能,指导编写和审查数据加载代码。它通过项目发现、版本检查及按后端类型选择规则,聚焦加载器、SSR、组件和变更流程。

主要功能

  1. 01按模式执行编写、审计或刷新
  2. 02发现应用配置并缓存结果
  3. 03检查 TanStack 依赖版本
  4. 04指导加载器并发与错误处理
  5. 05涵盖 SSR、组件和变更规则

适用场景

  1. 01检查路由加载器中的请求瀑布
  2. 02为应用刷新数据加载发现记录
  3. 03编写符合项目后端结构的服务函数

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/draekien/skills --skill tanstack-data-loading

安装后在 Claude Code 中输入/tanstack-data-loading确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
audit.md
backend-database.md
backend-service.md
versions.md
scripts/
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用法错误查看标准错误输出,修正调用后重新运行。

发现。 从代码中找出各字段:

字段值查找方式
backenddatabase、service、both阅读 createServerFn 处理程序。database:直接查询数据库客户端。service:通过 HTTP 调用其他服务。both:两种情况都有。
pendingUiroute-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 对应的参考文档:

版本

规则已针对 @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.0fetchQuery、prefetchQuery、ensureQueryData 及其无限查询变体,这些 API 在该版本中未弃用
staleTime: "static"@tanstack/react-query 5.79.0ensureQueryData(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 的 staleTime
    fetchInfiniteQuery(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 区块内显示错误。

评论

支持 Markdown