mcpField
D
data-loading

data-loading

github.com/tanstack开发15.3k215 次安装4 天前更新
代码仓库

关于 data-loading

面向 TanStack Router 开发者的路由数据加载技能,涵盖 loader 生命周期、缓存、待处理状态和错误处理。还说明 SSR 环境边界及如何在数据变更后刷新路由数据。

主要功能

  1. 01路由 loader 生命周期与参数
  2. 02基于搜索参数的 loader 缓存键
  3. 03过期再验证缓存设置
  4. 04待处理与延迟数据模式
  5. 05上下文与错误处理
  6. 06变更后的路由失效刷新

适用场景

  1. 01按路径和搜索参数加载路由数据
  2. 02调整 loader 结果的缓存时效与复用
  3. 03在路由数据加载时显示状态界面
  4. 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/历史记录更新时,路由器都会按以下顺序执行:

  1. 路由匹配(自上而下)
    • route.params.parse
    • route.validateSearch
  2. 路由预加载(串行)
    • route.beforeLoad
    • route.onError → route.errorComponent
  3. 路由加载(并行)
    • route.component.preload?
    • route.loader
      • route.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 — 预加载期间为 true
  • location — 当前 location 对象
  • parentMatchPromise — 父路由匹配的 Promise
  • route — 路由对象本身
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 协调外部缓存

评论

支持 Markdown