关于 data-loading
面向 TanStack Router 开发者的路由数据加载技能,涵盖 loader 生命周期、缓存、待处理状态和错误处理。还说明 SSR 环境边界及如何在数据变更后刷新路由数据。
主要功能
- 01路由 loader 生命周期与参数
- 02基于搜索参数的 loader 缓存键
- 03过期再验证缓存设置
- 04待处理与延迟数据模式
- 05上下文与错误处理
- 06变更后的路由失效刷新
适用场景
- 01按路径和搜索参数加载路由数据
- 02调整 loader 结果的缓存时效与复用
- 03在路由数据加载时显示状态界面
- 04创建、更新或删除后刷新路由数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill data-loading安装后在 Claude Code 中输入/data-loading确认已加载。
详细介绍
数据加载
设置
返回数据的基础加载器,通过 useLoaderData 使用:
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
component: PostsComponent,
})
function PostsComponent() {
const posts = Route.useLoaderData()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
在代码拆分组件中,使用 getRouteApi,而不是导入 Route:
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
function PostsComponent() {
const posts = routeApi.useLoaderData()
return <ul>{/* ... */}</ul>
}
路由加载生命周期
每次 URL/历史记录更新时,路由器都会按以下顺序执行:
- 路由匹配(自上而下)
route.params.parseroute.validateSearch
- 路由预加载(串行)
route.beforeLoadroute.onError→route.errorComponent
- 路由加载(并行)
route.component.preload?route.loaderroute.pendingComponent(可选)route.component
route.onError→route.errorComponent
要点:beforeLoad 在 loader 之前运行。父路由的 beforeLoad 在子路由的 beforeLoad 之前运行。在 beforeLoad 中抛出错误会阻止所有子路由加载。
核心模式
使用 loaderDeps 为搜索参数驱动的缓存键声明依赖项
加载器不会直接接收搜索参数。使用 loaderDeps 声明哪些搜索参数会影响缓存键:
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
validateSearch: (search) => ({
offset: Number(search.offset) || 0,
limit: Number(search.limit) || 10,
}),
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})
依赖项发生变化时,无论 staleTime 如何设置,路由都会重新加载。
SWR 缓存配置
TanStack Router 内置了 Stale-While-Revalidate 缓存,其缓存键由路由解析后的路径名和 loaderDeps 决定。
默认值:
staleTime: 0 — 数据始终被视为过期,在路由重新匹配时于后台重新加载preloadStaleTime: 30 秒 — 预加载的数据在 30 秒内不会重新获取gcTime: 30 分钟 — 未使用的缓存条目在 30 分钟后进行垃圾回收
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 10_000, // 10s: data considered fresh for 10 seconds
gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes
})
完全禁用 SWR 缓存:
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
全局设置:
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
等待状态(pendingComponent / pendingMs / pendingMinMs)
默认情况下,等待组件会在 1 秒后显示(pendingMs: 1000),并至少显示 500 毫秒(pendingMinMs: 500),以避免闪烁。
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
pendingMs: 500,
pendingMinMs: 300,
pendingComponent: () => <div>Loading posts...</div>,
component: PostsComponent,
})
使用 createRootRouteWithContext 设置路由器上下文(工厂模式)
createRootRouteWithContext 是一个返回函数的工厂。你必须调用两次——第一次传入泛型类型,第二次传入路由选项:
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface MyRouterContext {
auth: { userId: string }
fetchPosts: () => Promise<Post[]>
}
// NOTE: double call — createRootRouteWithContext<Type>()({...})
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})
创建路由器时提供上下文:
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
auth: { userId: '123' },
fetchPosts,
},
})
在加载器和 beforeLoad 中使用:
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
若要将 React Hook 的值传入路由器上下文,请在 RouterProvider 上方调用 Hook,并通过 context 属性注入:
import { RouterProvider } from '@tanstack/react-router'
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}
通过 beforeLoad 设置路由级上下文:
export const Route = createFileRoute('/posts')({
beforeLoad: ({ context }) => ({
fetchPosts: context.fetchPosts,
}),
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
当 TanStack Start 使用路由器时,确保实现兼容 SSR。在浏览器事件处理程序中,使用相对路径 fetch('/api/posts') 没有问题,但 Node 和许多服务端运行时在 SSR 期间需要绝对 URL。在 Start 中获取应用内部数据时,请从加载器调用服务端函数:
import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(() => {
return db.posts.findMany()
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})
只有在确实需要 HTTP 边界时,才使用服务端路由和根据来源构造的绝对 URL。不要对生产环境的来源进行硬编码。
延迟数据加载
对于非关键数据,在加载器中返回未等待的 Promise。使用 Await 组件进行渲染:
import { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
// Slow data — do NOT await
const slowDataPromise = fetchComments(postId)
// Fast data — await
const post = await fetchPost(postId)
return { post, deferredComments: slowDataPromise }
},
component: PostComponent,
})
function PostComponent() {
const { post, deferredComments } = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<Await
promise={deferredComments}
fallback={<div>Loading comments...</div>}
>
{(comments) => (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.body}</li>
))}
</ul>
)}
</Await>
</div>
)
}
变更后的失效处理
router.invalidate() 会强制所有活动路由加载器重新运行,并将所有缓存数据标记为过期:
import { useRouter } from '@tanstack/react-router'
function AddPostButton() {
const router = useRouter()
const handleAdd = async () => {
await createPost({ title: 'New post' })
await router.invalidate({ sync: true })
}
return <button onClick={handleAdd}>Add Post</button>
}
如果下一步需要刷新的加载器数据,请使用 await router.invalidate({ sync: true })。
将变更和失效处理视为一个工作流。变更必须先完成持久化,之后才能开始失效处理;加载器必须从同一个权威存储中读取数据。通过渲染后的路由验证创建、更新和删除操作,并包含重新加载页面后的验证;本地组件状态可能会掩盖过期的加载器数据或未持久化的写入。
错误处理
import {
createFileRoute,
ErrorComponent,
useRouter,
} from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
if (error instanceof CustomError) {
return <div>{error.message}</div>
}
return (
<div>
<ErrorComponent error={error} />
<button
onClick={() => {
// For loader errors, invalidate to re-run loader + reset boundary
router.invalidate()
}}
>
Retry
</button>
</div>
)
},
})
加载器参数
loader 函数接收:
params— 解析后的路径参数deps— 来自loaderDeps的对象context— 合并后的父级上下文和 beforeLoad 上下文abortController— 路由卸载或变为过期状态时取消cause—'enter'、'stay'或'preload'preload— 预加载期间为truelocation— 当前 location 对象parentMatchPromise— 父路由匹配的 Promiseroute— 路由对象本身
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId }, abortController }) =>
fetchPost(postId, { signal: abortController.signal }),
})
常见错误
CRITICAL:假设加载器只在服务器上运行
TanStack Router 以客户端优先。加载器默认在客户端运行。使用 TanStack Start 进行 SSR 时,加载器也会在服务器上运行,但默认的思维模型是客户端执行。
// WRONG — this will crash in the browser
export const Route = createFileRoute('/posts')({
loader: async () => {
const fs = await import('fs') // Node.js only!
return JSON.parse(fs.readFileSync('...')) // fails in browser
},
})
// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
不要直接在加载器中执行数据库查询、文件系统访问或仅限服务器端的代码。在 TanStack Start 中,将这些操作放在服务器函数中,并从加载器调用该函数。不要在 SSR 加载器中使用相对路径的 fetch('/api/...')。
MEDIUM:不了解 staleTime 默认值为 0
默认 staleTime 为 0。这意味着每次路由重新匹配时,数据都会在后台重新加载。这是有意设计的,可确保数据保持最新。但如果数据获取成本高或内容静态,请设置 staleTime:
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 60_000, // Consider fresh for 1 minute
})
HIGH:在错误组件中使用 reset(),而不是 router.invalidate()
reset() 只会重置错误边界 UI,不会重新运行加载器。对于加载器错误,请使用 router.invalidate(),它会重新运行加载器并重置错误边界:
// WRONG — resets boundary but loader still has stale error
function PostErrorComponent({ error, reset }) {
return <button onClick={reset}>Retry</button>
}
// CORRECT — re-runs loader and resets the error boundary
function PostErrorComponent({ error }) {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Retry</button>
}
HIGH:createRootRouteWithContext 缺少第二对括号
createRootRouteWithContext<Type>() 是一个工厂函数,会返回一个函数。必须调用两次:
// WRONG — missing second call, passes options to the factory
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({
component: RootComponent,
})
// CORRECT — factory()({options})
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({
component: RootComponent,
})
HIGH:在 beforeLoad 或 loader 中使用 React 钩子
beforeLoad 和 loader 不是 React 组件。不能在其中调用钩子。使用路由器上下文注入钩子中的值:
// WRONG — hooks cannot be called outside React components
export const Route = createFileRoute('/posts')({
loader: () => {
const auth = useAuth() // This will crash!
return fetchPosts(auth.userId)
},
})
// CORRECT — inject hook values via router context
// In your App component:
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In your route:
export const Route = createFileRoute('/posts')({
loader: ({ context: { auth } }) => fetchPosts(auth.userId),
})
HIGH:属性顺序会影响 TypeScript 类型推断
路由器会将前面属性的类型推断结果用于后续属性。在 loader 之后声明 beforeLoad,意味着加载器无法获知 beforeLoad 中的上下文:
// WRONG — context.user is unknown because beforeLoad declared after loader
export const Route = createFileRoute('/admin')({
loader: ({ context }) => fetchData(context.user),
beforeLoad: () => ({ user: getUser() }),
})
// CORRECT — validateSearch → loaderDeps → beforeLoad → loader
export const Route = createFileRoute('/admin')({
beforeLoad: () => ({ user: getUser() }),
loader: ({ context }) => fetchData(context.user),
})
HIGH:从 loaderDeps 返回整个 search 对象
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search
// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })
返回整个 search 对象,意味着无关参数的变化(例如 sortDirection、viewMode)会触发不必要的重新加载,因为对整个对象进行深度相等性检查会失败。
权衡
- 客户端优先的加载器与 SSR 预期之间的权衡:加载器默认在客户端运行。使用 SSR(TanStack Start)时,加载器会在客户端和服务器上运行。浏览器专用 API 默认可用,但在 SSR 下会出错。仅限服务器端的 API(fs、db)默认会出错,但在 Start 服务器函数中可用。请参阅 router-core/ssr/SKILL.md。
- 内置 SWR 缓存与外部缓存协调之间的权衡:路由器内置了缓存。使用 TanStack Query 时,将
defaultPreloadStaleTime: 0设为0,以避免重复缓存。请参阅 compositions/router-query/SKILL.md。
交叉引用
- 另请参阅:router-core/search-params/SKILL.md —
loaderDeps使用经过验证的搜索参数作为缓存键 - 另请参阅:compositions/router-query/SKILL.md — 了解如何使用 TanStack Query 协调外部缓存