tanstack-data-loading
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack data loading
TanStack 数据加载
The first argument selects the mode. With no mode argument, infer it: a request to audit, find waterfalls or review existing code is ; a request to rediscover or bust the cache is ; anything else is writing.
auditrefresh- — read references/audit.md before starting, and run it over
audit [--fix] [path].path - — rerun Discovery for the app containing
refresh [path], write the result, and stop.path - Writing — apply the rules below to the code being written or reviewed, within if given.
path
Every mode starts with Discovery. The rules branch on what it records: if you guess the backend shape, you apply the wrong server function rules and the wrong write rules.
第一个参数用于选择模式。如果未提供模式参数,则进行推断:要求审计、查找请求瀑布或审查现有代码时使用 ;要求重新发现或清除缓存时使用 ;其他情况均视为编写。
auditrefresh- —— 开始前阅读 references/audit.md,并对
audit [--fix] [path]执行审计。path - —— 对包含
refresh [path]的应用重新执行发现流程,写入结果,然后停止。path - 编写 —— 对正在编写或审查的代码应用以下规则;如果提供了 ,则限定在该路径内。
path
每种模式都从发现流程开始。规则会根据发现流程记录的信息进行分支处理:如果错误判断了 backend 形态,就会应用错误的 server function 规则和错误的写入规则。
Available scripts
可用脚本
- —
scripts/discovery.pyandreadthe discovery cache atwrite, one discovery entry per app root;<repo-root>/.draekien/tanstack-data-loading/discovery.jsonprints the installedversionsversions.@tanstacklists the exit codes.--help
- —— 在
scripts/discovery.py中读取和写入发现缓存,每个应用根目录对应一个发现条目;<repo-root>/.draekien/tanstack-data-loading/discovery.json会打印已安装的versions版本。@tanstack会列出退出码。--help
Discovery
发现
App root. The nearest ancestor of (or of the current directory) whose depends on . If none matches, or the task spans several apps, ask the user which app.
pathpackage.json@tanstack/react-startRead. Run the script from this skill's directory, passing the app root relative to the repo root ( for a single-package repo):
.bash
uv run scripts/discovery.py read --app apps/web --repo <repo-root>| Exit | Meaning | Next |
|---|---|---|
| 0 | Fresh discovery entry printed | Use it, and apply its |
| 4 | No discovery entry | Discover and write. |
| 5 | Older than the script's maximum age, or an installed | Discover and write. |
| 3 | Cache file or an installed package manifest unreadable | Read stderr. For the cache file, tell the user and delete it only after they agree, since it holds every app's discovery entry. For a manifest, ask the user to reinstall dependencies. |
| 2 | Usage error | Read stderr, correct the call, rerun. |
Discover. Find each field from the code:
| Field | Values | How to find it |
|---|---|---|
| | Read the |
| | |
| list of commands | The repo's lint, typecheck and test commands from |
| list of strings | Run |
Write. The script adds and the installed , and rejects unknown values:
discoveredAtversionsbash
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": []}
EOFFirst write in a repo. Exit 6 means is missing: ask the user before creating it. When does not exist yet, ask whether to add to ; the discovery entries hold one developer's local discovery. If the user declines , keep the discovered fields for this task only and say the discovery cache was not written.
.draekien/.draekien/tanstack-data-loading/discovery.json.draekien/tanstack-data-loading/.gitignore.draekien/Discovery is done when or exits 0 for the app, or when the user declined the cache and every field is known.
readwriteBackend rules. Before writing or auditing any server function, mutation or write path, read the reference for :
backend- — references/backend-database.md.
database - — references/backend-service.md.
service - — read both. Apply each to the server functions whose handlers match it; a handler that does both applies each reference to its own part.
both
应用根目录。 (或当前目录)向上查找时,最近一个 依赖 的祖先目录。如果没有匹配项,或者任务涉及多个应用,则询问用户要使用哪个应用。
pathpackage.json@tanstack/react-start读取。 从此 skill 的目录运行脚本,并传入应用根目录相对于仓库根目录的路径(单包仓库使用 ):
.bash
uv run scripts/discovery.py read --app apps/web --repo <repo-root>| 退出码 | 含义 | 后续操作 |
|---|---|---|
| 0 | 已打印最新的发现条目 | 使用该条目,并应用其中的 |
| 4 | 没有发现条目 | 执行发现和写入。 |
| 5 | 条目早于脚本允许的最大时长,或已安装的 | 执行发现和写入。 |
| 3 | 缓存文件或已安装的 package manifest 无法读取 | 阅读 stderr。对于缓存文件,先告知用户,并且只有获得同意后才能删除,因为它保存了所有应用的发现条目。对于 manifest,请用户重新安装依赖。 |
| 2 | 用法错误 | 阅读 stderr,修正调用后重新运行。 |
发现。 从代码中查找每个字段:
| 字段 | 值 | 查找方式 |
|---|---|---|
| | 阅读 |
| | 当 router 设置了 |
| 命令列表 | 从 |
| 字符串列表 | 运行 |
写入。 脚本会添加 和已安装的 ,并拒绝未知值:
discoveredAtversionsbash
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/当应用的读取或写入退出码为 0,或者用户拒绝使用缓存且所有字段均已确定时,即完成发现流程。
Backend 规则。 在编写或审计任何 server function、mutation 或写入路径之前,根据 阅读对应参考资料:
backend- —— references/backend-database.md。
database - —— references/backend-service.md。
service - —— 两份都阅读。分别将其应用于匹配的 server function;如果某个 handler 同时包含两种逻辑,则对各自对应的部分应用相应参考资料。
both
Versions
版本
Rules were checked against 5.104.2, 1.170.44 and 1.169.1. Before applying a rule that uses an API below, compare the installed versions (the discovery entry's , or the command) to the minimum and use the substitute when the installed version is lower:
@tanstack/react-query@tanstack/react-router@tanstack/react-startversionsversions| Rule uses | Needs | Below the minimum |
|---|---|---|
| | |
| | |
| | |
mutation | | disable the trigger while |
| | |
| | the suffix is a naming convention; nothing blocks the import |
When an installed major version is newer than the checked one, or an API a rule uses is missing from the installed types, and the discovery entry has no , read references/versions.md before applying any rule.
versionNotes以下规则基于 5.104.2、 1.170.44 和 1.169.1 进行核对。应用使用下列 API 的规则之前,将已安装版本(发现条目中的 ,或 命令的输出)与最低版本进行比较;如果已安装版本较低,则使用替代方案:
@tanstack/react-query@tanstack/react-router@tanstack/react-startversionsversions| 规则使用的 API | 最低版本 | 低于最低版本时的替代方案 |
|---|---|---|
| | |
| | |
| | |
mutation 的 | | 在 |
| | |
阻止客户端导入 | | 后缀仅是一种命名约定,不会阻止导入 |
如果已安装的主版本高于核对时使用的版本,或者规则使用的 API 未出现在已安装的类型定义中,并且发现条目中没有 ,则应用任何规则之前先阅读 references/versions.md。
versionNotesLoaders
Loader
Scope: the option of every file route under the app root.
loaderA read is critical when the route must not render without it or must render something else when it fails: the main content, existence (404), authorization (403), a redirect, a limit gate, form defaults. Every other read is unawaited.
-
Critical read:. Awaiting it in the loader sends a 404 or 403 to
await context.queryClient.query(options)ornotFoundComponentbefore render; let the error reach them, or throwerrorComponent.notFound() -
Unawaited read, including hidden variants such as a toggle's second period:. Without
void context.queryClient.query(options).catch(noop)the rejection is unhandled on the server; the component's.catch(noop)still rethrows the error to its boundary.useSuspenseQuery -
Start every unawaited read before the first. A read started after an
awaitis a waterfall.await -
Independent critical reads: one.
await Promise.all([...])ts✗ 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)), ]) -
A readthat needs read
b's result is a waterfall that cannot be removed:aabLoader critical critical , thenconst x = await query(a)await query(b(x))critical unawaited , thenconst x = await query(a): the one unawaited read started after anvoid query(b(x)).catch(noop)awaitunawaited unawaited , started before the firstvoid query(a).then((x) => query(b(x))).catch(noop)await -
The loader and the component use the sameobject. A different key in the component fetches the data a second time.
queryOptions -
A route a signed-out visitor can open starts reads that require a session only in its signed-in branch. Otherwise every signed-out visit fires calls that throw.
-
Replace deprecatedmethods:
QueryClientDeprecated Replacement fetchQuery(options)query(options)prefetchQuery(options)void query(options).catch(noop)ensureQueryData(options), which behaves the same. If a mutation invalidates the key, usequery({ ...options, staleTime: "static" })instead: a later loader run withstaleTime: Infinityreturns the invalidated cached data, so the route decides its branch, redirect or gate from old data, while"static"refetches first. AInfinitypassed to onestaleTimecall does not stay on the query: whether invalidation refetches a mounted page depends on the component'squery()useSuspenseQuerystaleTimefetchInfiniteQuery(options)infiniteQuery(options)prefetchInfiniteQuery(options)void infiniteQuery(options).catch(noop)ensureInfiniteQueryData(options)with the sameinfiniteQuery(...)choice asstaleTimeensureQueryData -
Do not defer reads to scroll or interaction, and do not add hover prefetching beyond the router's. An unawaited read already streams without blocking the page; deferring it adds a request after render, and no TanStack doc defines how deferral behaves with
defaultPreload: "intent".useSuspenseQuery
适用范围:应用根目录下每个 file route 的 选项。
loader当 route 在缺少某次读取时无法渲染,或者读取失败时必须呈现其他内容时,该读取属于关键读取:主要内容、存在性检查(404)、授权检查(403)、重定向、限额拦截、表单默认值。其他所有读取均属于未等待读取。
-
关键读取:。在 loader 中 await 它,会在渲染前将 404 或 403 传递给
await context.queryClient.query(options)或notFoundComponent;让错误传递给它们,或抛出errorComponent。notFound() -
未等待读取,包括切换开关的第二个时段等隐藏变体:。如果没有
void context.queryClient.query(options).catch(noop),服务端会将 rejection 视为未处理;组件中的.catch(noop)仍会将错误重新抛给对应的 boundary。useSuspenseQuery -
每个未等待读取都必须在第一次之前启动。在
await之后启动的读取会形成请求瀑布。await -
相互独立的关键读取:使用一次。
await Promise.all([...])ts✗ 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的结果,则该请求瀑布无法消除:aabLoader 关键读取 关键读取 ,然后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 -
Loader 和组件应使用同一个对象。组件使用不同的 key 时,会再次获取数据。
queryOptions -
对于未登录访问者也能打开的 route,仅在已登录分支中启动需要 session 的读取。否则每次未登录访问都会触发抛出错误的调用。
-
替换已弃用的方法:
QueryClient已弃用方法 替代方案 fetchQuery(options)query(options)prefetchQuery(options)void query(options).catch(noop)ensureQueryData(options),其行为相同。如果 mutation 会使该 key 失效,则改用query({ ...options, staleTime: "static" }):后续 loader 使用staleTime: Infinity时会返回已经失效但仍被缓存的数据,导致 route 根据旧数据决定分支、重定向或拦截,而"static"会先重新获取数据。传递给单次Infinity调用的query()不会附着在该 query 上;失效后是否重新获取已挂载页面的数据,取决于组件的staleTimeuseSuspenseQuerystaleTimefetchInfiniteQuery(options)infiniteQuery(options)prefetchInfiniteQuery(options)void infiniteQuery(options).catch(noop)ensureInfiniteQueryData(options),并采用与infiniteQuery(...)相同的ensureQueryData选择staleTime -
不要将读取推迟到滚动或交互时,也不要在 router 的之外添加 hover 预取。未等待读取本身已经以流式方式传输,不会阻塞页面;将其推迟只会增加一次渲染后的请求,而且没有任何 TanStack 文档定义这种延迟行为在
defaultPreload: "intent"下的表现。useSuspenseQuery
Streaming and SSR
流式传输与 SSR
Scope: the router file and every route's option.
ssr- The rules here hold only when the router file calls from
setupRouterSsrQueryIntegration. If it does not, report that as the first finding.@tanstack/react-router-ssr-query - An unawaited read runs on the server and streams to the client without blocking the server response. A critical read blocks the response until it resolves.
- and loader reads run on the server.
useSuspenseQuerydoes not; its data loads after hydration, so a route component that reads withuseQueryis a waterfall.useQuery - runs
ssr: "data-only"andbeforeLoadon the server and renders the component only on the client. Do not rely on unawaited reads streaming underloader, and do not recommend it as a fix; no official doc states the behaviour. Check it in the running app when the task depends on it.ssr: "data-only" - Do not configure route code splitting: Start always enables it, and the loader stays in the main chunk.
适用范围:router 文件以及每个 route 的 选项。
ssr- 仅当 router 文件调用了 中的
@tanstack/react-router-ssr-query时,此处的规则才成立。如果未调用,应首先报告此问题。setupRouterSsrQueryIntegration - 未等待读取在服务端运行并流向客户端,不会阻塞服务端响应。关键读取会阻塞响应,直至解析完成。
- 和 loader 读取在服务端运行。
useSuspenseQuery不会在服务端运行;其数据会在 hydration 后加载,因此使用useQuery读取数据的 route 组件会形成请求瀑布。useQuery - 会在服务端运行
ssr: "data-only"和beforeLoad,并且仅在客户端渲染组件。不要依赖未等待读取在该模式下进行流式传输,也不要将其作为修复建议;没有任何官方文档说明此行为。当任务依赖于具体表现时,应在实际运行的应用中检查。loader - 不要配置 route 代码分割:Start 始终会启用它,而 loader 会保留在 main chunk 中。
Components
组件
Scope: components that read query data with suspense.
-
Two or more suspense queries in one component: onecall. Separate
useSuspenseQueriescalls in one component are a waterfall: the second starts after the first resolves.useSuspenseQuerytsx✗ const { data: order } = useSuspenseQuery(orderQueryOptions(id)) const { data: notes } = useSuspenseQuery(notesQueryOptions(id)) ✓ const [{ data: order }, { data: notes }] = useSuspenseQueries({ queries: [orderQueryOptions(id), notesQueryOptions(id)], }) -
A child that suspends on a query its route could know about: start that read in the route loader. Otherwise the child's request starts only after the parent renders.
-
Each unawaited read renders inside its own Suspense section:from
<CatchBoundary getResetKey={...} errorComponent={...}>around@tanstack/react-router, inside<React.Suspense fallback={...}>where the error UI offers a retry. The rest of the page renders while the section streams, and its error stays inside it. A critical read needs no Suspense section.QueryErrorResetBoundary -
A query key that changes on the same route (search param, tab, page, period): make the change inside.
React.startTransitionhas nouseSuspenseQuery; without the transition, React replaces the content with the fallback.placeholderData -
: the router sets
pendingUi: route-level, rendered inside the app's shell layout. It renders afterdefaultPendingComponent(default 1000) and stays at leastdefaultPendingMs(default 500).defaultPendingMinMs: add nopendingUi: per-section; if the user chose this while the router sets one, leave it until they ask for its removal.defaultPendingComponent
适用范围:使用 suspense 读取 query 数据的组件。
-
一个组件中存在两个或更多 suspense query 时,使用一次。在同一组件中分别调用
useSuspenseQueries会形成请求瀑布:第二个调用会在第一个解析完成后才开始。useSuspenseQuerytsx✗ const { data: order } = useSuspenseQuery(orderQueryOptions(id)) const { data: notes } = useSuspenseQuery(notesQueryOptions(id)) ✓ const [{ data: order }, { data: notes }] = useSuspenseQueries({ queries: [orderQueryOptions(id), notesQueryOptions(id)], }) -
如果子组件会因 route 已知的数据而 suspend,则应在 route loader 中启动该读取。否则子组件的请求只会在父组件渲染后开始。
-
每个未等待读取都应在独立的 Suspense section 中渲染:使用中的
@tanstack/react-router包裹<CatchBoundary getResetKey={...} errorComponent={...}>;如果错误 UI 提供重试功能,则将其放在<React.Suspense fallback={...}>内。section 流式传输期间,页面其余部分仍可渲染,并且错误会保留在该 section 内。关键读取不需要 Suspense section。QueryErrorResetBoundary -
同一 route 中的 query key 发生变化时(搜索参数、标签页、页码、时间范围),应在内进行更改。
React.startTransition没有useSuspenseQuery;如果没有 transition,React 会用 fallback 替换原有内容。placeholderData -
:router 设置了
pendingUi: route-level,并在应用的 shell layout 内渲染。它会在defaultPendingComponent之后渲染(默认 1000),并且至少保留defaultPendingMs(默认 500)。defaultPendingMinMs:不要添加pendingUi: per-section;如果用户选择了此模式但 router 已设置该组件,则保持现状,直到用户要求将其移除。defaultPendingComponent
Mutations
Mutation
Scope: every .
useMutation-
The server function returns the saved entity.writes it with
onSuccess.queryClient.setQueryData(key, saved) -
invalidates only keys derived from the change (lists, counts, totals) and does not await the invalidation. A promise returned or awaited in
onSuccesskeepsonSuccesstrue until every invalidated query has refetched.isPendingts✗ onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orderKeys.all }) } ✓ onSuccess: (saved) => { queryClient.setQueryData(orderKeys.detail(saved.id), saved) void queryClient.invalidateQueries({ queryKey: orderKeys.lists() }) } -
Never invalidate the key just written; invalidation refetches it. To invalidate a prefix that contains it, skip the written key with a:
predicatetsvoid queryClient.invalidateQueries({ queryKey: orderKeys.all, predicate: (query) => query.queryKey.length !== written.length || query.queryKey.some((part, index) => part !== written[index]), }) -
Saves to one entity that can overlap (autosave, rapid toggles): setto a string naming the entity, such as
scope: { id }. Mutations with the same`order-${orderId}`run one at a time in call order. A refetch after each save does not fix ordering: the responses can still arrive out of order.scope.id -
A mutation that changes which data the user may see (switching account or workspace) awaits its invalidation, so the next screen never renders with the previous account's data. Every other mutation follows the rule above.
适用范围:每个 。
useMutation-
Server function 返回已保存的实体。使用
onSuccess写入该实体。queryClient.setQueryData(key, saved) -
仅使由本次更改派生出的 key(列表、计数、总计)失效,并且不等待失效操作完成。在
onSuccess中返回或 await Promise,会使onSuccess一直保持为 true,直到所有被失效的 query 都完成重新获取。isPendingts✗ onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: orderKeys.all }) } ✓ onSuccess: (saved) => { queryClient.setQueryData(orderKeys.detail(saved.id), saved) void queryClient.invalidateQueries({ queryKey: orderKeys.lists() }) } -
绝不要使刚刚写入的 key 失效;失效操作会重新获取它。如果需要使包含该 key 的前缀失效,请使用跳过刚写入的 key:
predicatetsvoid queryClient.invalidateQueries({ queryKey: orderKeys.all, predicate: (query) => query.queryKey.length !== written.length || query.queryKey.some((part, index) => part !== written[index]), }) -
如果对同一个可能发生重叠的实体执行保存操作(autosave、快速切换),应设置,并使用标识该实体的字符串,例如
scope: { id }。具有相同`order-${orderId}`的 mutation 会按照调用顺序逐一执行。每次保存后重新获取无法解决顺序问题,因为响应仍可能乱序到达。scope.id -
如果 mutation 会改变用户有权查看的数据(切换账户或 workspace),则应等待失效操作完成,确保下一个界面绝不会先渲染旧账户的数据。其他 mutation 均遵循上述规则。
Server functions
Server function
Scope: every .
createServerFn- Put exports in
createServerFn. Client code may import them; the build replaces each handler with a request stub. Server-only helpers go in*.functions.ts, which import protection blocks from client code.*.server.ts - Validate input: .
.validator(z.object({ ... })) - Each call from the client is one HTTP request with a fixed cost: the request, plus every middleware it runs. Start neither batches nor deduplicates calls: over three server functions sends three concurrent requests, and two identical calls send two. Deduplicate identical reads through the query cache by calling the server function only from a
Promise.all.queryFn - Do not merge every read on a page into one server function. The page then waits for its slowest part, Suspense sections stop streaming separately, and every mutation refetches all of it.
- Middleware runs on every call to every server function that uses it. Shared middleware computes only what every one of those functions reads; a value only some read goes in its own middleware.
- Statements sent on one database connection, including every statement in one transaction, run one at a time. over them saves no time; reduce the number of statements instead.
Promise.all - Apply the backend reference named under Discovery, "Backend rules", to every server function.
适用范围:每个 。
createServerFn- 将 导出放在
createServerFn中。客户端代码可以导入它们;构建过程会将每个 handler 替换为 request stub。仅供服务端使用的 helper 应放在*.functions.ts中,其导入保护机制会阻止客户端代码导入。*.server.ts - 验证输入:。
.validator(z.object({ ... })) - 客户端的每次调用都会发起一个具有固定成本的 HTTP 请求:请求本身,以及它所经过的每项 middleware。Start 既不会批处理调用,也不会对调用去重:对三个 server function 使用 会发送三个并发请求,而两次相同的调用也会发送两个请求。通过 query cache 对相同读取进行去重,方法是只在
Promise.all中调用 server function。queryFn - 不要将页面上的所有读取合并到一个 server function 中。否则页面必须等待其中最慢的部分,Suspense section 无法分别流式传输,而且每次 mutation 都会重新获取全部数据。
- Middleware 会在每次调用使用它的 server function 时运行。共享 middleware 只计算所有这些 function 都会读取的内容;仅部分 function 读取的值应放入各自的 middleware。
- 通过同一个 database connection 发送的语句,包括同一 transaction 中的所有语句,都会依次执行。对它们使用 无法节省时间;应改为减少语句数量。
Promise.all - 对每个 server function 应用发现流程中“Backend 规则”所指定的 backend 参考资料。
Gotchas
常见陷阱
- does not accept infinite queries. An infinite query in the same component still starts after the others resolve; start it in the loader.
useSuspenseQueries - After converting to , every test mock of
useSuspenseQueriesmust export@tanstack/react-query.useSuspenseQueries - on cached rows fails: structural sharing returns an equal copy, not the same object. Use
toBe.toEqual - Once a loader awaits a read, a 404 from it right after a write (a backend that updates its read model asynchronously) shows the route's error component instead of an error inside a Suspense section.
- 不接受 infinite query。同一组件中的 infinite query 仍会在其他 query 解析完成后才开始;应在 loader 中启动它。
useSuspenseQueries - 转换为 后,每个 mock
useSuspenseQueries的测试都必须导出@tanstack/react-query。useSuspenseQueries - 对缓存行使用 会失败:结构共享返回的是内容相等的副本,而不是同一个对象。请使用
toBe。toEqual - Loader 一旦 await 了某次读取,写入后立即由该读取返回的 404(backend 异步更新 read model)会显示 route 的 error component,而不是显示在 Suspense section 内部。