框架
版本

数据加载

数据加载是 Web 应用程序的常见问题,并且与路由相关。当为您的应用加载页面时,理想情况是尽早并行获取并满足页面所有异步需求。路由器是协调这些异步依赖项的最佳位置,因为它通常是您的应用中唯一在内容呈现之前知道用户要去往何处的地方。

您可能熟悉 Next.js 中的 getServerSideProps 或 Remix/React-Router 中的 loader。TanStack Router 具有类似的功能,可以在每个路由的基础上并行预加载/加载资源,使其能够通过 suspense 尽快渲染。

除了路由器的这些正常期望之外,TanStack Router 还更进一步,提供了内置的 SWR 缓存,这是一个用于路由加载器的长期内存缓存层。这意味着您可以使用 TanStack Router 既可以预加载路由数据,使其立即加载,也可以临时缓存先前访问过的路由数据以供以后再次使用。

路由加载生命周期

每次检测到 URL/历史记录更新时,路由器都会执行以下序列

  • 路由匹配(自顶向下)
    • route.params.parse
    • route.validateSearch
  • 路由预加载(串行)
    • route.beforeLoad
    • route.onError
      • route.errorComponent / parentRoute.errorComponent / router.defaultErrorComponent
  • 路由加载(并行)
    • route.component.preload?
    • route.loader
      • route.pendingComponent(可选)
      • route.component
    • route.onError
      • route.errorComponent / parentRoute.errorComponent / router.defaultErrorComponent

要使用 Router 缓存还是不使用 Router 缓存?

TanStack 的路由器缓存很可能非常适合大多数中小型应用程序,但重要的是要了解使用它与更强大的缓存解决方案(如 TanStack Query)之间的权衡。

TanStack Router 缓存的优点

  • 内置,易于使用,没有额外的依赖项
  • 处理每个路由的去重、预加载、加载、stale-while-revalidate、后台重新获取
  • 粗粒度失效(一次性使所有路由和缓存失效)
  • 自动垃圾回收
  • 非常适合路由之间共享少量数据的应用程序
  • SSR “开箱即用”

TanStack Router 缓存的缺点

  • 没有持久化适配器/模型
  • 路由之间没有共享的缓存/去重
  • 没有内置的 mutation API(在许多示例中提供了一个基本的 useMutation hook,这可能足以满足许多用例)
  • 没有内置的缓存级别乐观更新 API(您仍然可以使用来自 useMutation hook 之类的临时状态在组件级别实现此目的)

提示

如果您立即知道您想要或需要使用更强大的工具(如 TanStack Query),请跳至外部数据加载指南。

使用 Router 缓存

路由器缓存是内置的,并且像从任何路由的 loader 函数返回数据一样容易。让我们学习如何使用!

路由 loader

当加载路由匹配时,将调用路由 loader 函数。它们使用单个参数调用,该参数是一个包含许多有用属性的对象。我们稍后会介绍这些属性,但首先,让我们看一个路由 loader 函数的示例

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})

loader 参数

loader 函数接收一个带有以下属性的单个对象

  • abortController - 路由的 abortController。当路由卸载或 Route 不再相关且当前 loader 函数调用过时时,其信号将被取消。
  • cause - 当前路由匹配的原因,可以是 enterstay
  • context - 路由的上下文对象,它是以下项的合并联合
    • 父路由上下文
    • 此路由的上下文,由 beforeLoad 选项提供
  • deps - 从 Route.loaderDeps 函数返回的对象值。如果未定义 Route.loaderDeps,则将提供一个空对象。
  • location - 当前位置
  • params - 路由的路径参数
  • parentMatchPromise - Promise<RouteMatch>(根路由为 undefined
  • preload - 布尔值,当路由正在预加载而不是加载时为 true
  • route - 路由本身

使用这些参数,我们可以做很多很酷的事情,但首先,让我们看看如何控制它以及何时调用 loader 函数。

loader 消费数据

要从 loader 消费数据,请使用在您的 Route 对象上定义的 useLoaderData hook。

tsx
const posts = Route.useLoaderData()
const posts = Route.useLoaderData()

如果您无法直接访问您的路由对象(即,您位于当前路由的组件树深处),则可以使用 getRouteApi 来访问相同的 hook(以及 Route 对象上的其他 hook)。这应该优先于导入 Route 对象,Route 对象很可能会创建循环依赖。

tsx
import { getRouteApi } from '@tanstack/react-router'

// in your component

const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
import { getRouteApi } from '@tanstack/react-router'

// in your component

const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()

基于依赖的 Stale-While-Revalidate 缓存

TanStack Router 为路由加载器提供了一个内置的 Stale-While-Revalidate 缓存层,该缓存层基于路由的依赖项进行键控

  • 路由的完全解析的路径名
    • 例如 /posts/1/posts/2
  • loaderDeps 选项提供的任何其他依赖项
    • 例如 loaderDeps: ({ search: { pageIndex, pageSize } }) => ({ pageIndex, pageSize })

使用这些依赖项作为键,TanStack Router 将缓存从路由的 loader 函数返回的数据,并使用它来满足后续对同一路由匹配的请求。这意味着,如果路由的数据已在缓存中,它将立即返回,然后可能在后台重新获取,具体取决于数据的“新鲜度”。

Key 选项

为了控制路由器依赖项和“新鲜度”,TanStack Router 提供了大量选项来控制路由加载器的键控和缓存行为。让我们按照您最有可能使用的顺序查看它们

  • routeOptions.loaderDeps
    • 一个函数,为您提供路由器的搜索参数,并返回一个依赖项对象,供您的 loader 函数使用。当这些依赖项在导航之间发生更改时,无论 staleTime 如何,都会导致路由重新加载。依赖项使用深度相等性检查进行比较。
  • routeOptions.staleTime
  • routerOptions.defaultStaleTime
    • 尝试加载时,路由数据应被视为新鲜的毫秒数。
  • routeOptions.preloadStaleTime
  • routerOptions.defaultPreloadStaleTime
    • 尝试预加载时,路由数据应被视为新鲜的毫秒数。
  • routeOptions.gcTime
  • routerOptions.defaultGcTime
    • 路由数据在缓存中保留以进行垃圾回收之前的毫秒数。
  • routeOptions.shouldReload
    • 一个函数,接收相同的 beforeLoadloaderContext 参数,并返回一个布尔值,指示路由是否应重新加载。这提供了对路由何时应重新加载的又一层控制,超出 staleTimeloaderDeps,并且可以用于实现类似于 Remix 的 shouldLoad 选项的模式。

⚠️ 一些重要的默认值

  • 默认情况下,staleTime 设置为 0,这意味着路由的数据将始终被视为过时,并且当重新匹配路由时,将始终在后台重新加载。
  • 默认情况下,先前预加载的路由被视为新鲜 30 秒。这意味着,如果一个路由被预加载,然后在 30 秒内再次预加载,则第二次预加载将被忽略。这可以防止不必要的预加载过于频繁地发生。当正常加载路由时,将使用标准的 staleTime
  • 默认情况下,gcTime 设置为 30 分钟,这意味着任何在 30 分钟内未被访问的路由数据都将被垃圾回收并从缓存中删除。
  • router.invalidate() 将强制所有活动路由立即重新加载其加载器,并将每个缓存路由的数据标记为过时。

使用 loaderDeps 访问搜索参数

假设一个 /posts 路由通过搜索参数 offsetlimit 支持分页。为了使缓存唯一地存储此数据,我们需要通过 loaderDeps 函数访问这些搜索参数。通过显式标识它们,具有不同 offsetlimit/posts 的每个路由匹配都不会混淆!

一旦我们设置了这些依赖项,当依赖项更改时,路由将始终重新加载。

tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps: { offset, limit } }) =>
    fetchPosts({
      offset,
      limit,
    }),
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps: { offset, limit } }) =>
    fetchPosts({
      offset,
      limit,
    }),
})

使用 staleTime 控制数据被视为新鲜的时长

默认情况下,导航的 staleTime 设置为 0 毫秒(预加载为 30 秒),这意味着路由的数据将始终被视为过时,并且当路由匹配并导航到时,将始终在后台重新加载。

对于大多数用例来说,这是一个很好的默认值,但您可能会发现某些路由数据更静态或加载成本可能更高。 在这些情况下,您可以使用 staleTime 选项来控制路由数据在导航中被视为新鲜的时长。让我们看一个例子

tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  // Consider the route's data fresh for 10 seconds
  staleTime: 10_000,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  // Consider the route's data fresh for 10 seconds
  staleTime: 10_000,
})

通过将 10_000 传递给 staleTime 选项,我们告诉路由器将路由的数据视为新鲜 10 秒。这意味着,如果用户在上一次加载器结果的 10 秒内从 /about 导航到 /posts,则不会重新加载路由的数据。如果用户在 10 秒后从 /about 导航到 /posts,则路由的数据将在后台重新加载。

关闭 stale-while-revalidate 缓存

要禁用路由的 stale-while-revalidate 缓存,请将 staleTime 选项设置为 Infinity

tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: Infinity,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: Infinity,
})

您甚至可以通过在路由器上设置 defaultStaleTime 选项来为所有路由关闭此功能

tsx
const router = createRouter({
  routeTree,
  defaultStaleTime: Infinity,
})
const router = createRouter({
  routeTree,
  defaultStaleTime: Infinity,
})

使用 shouldReloadgcTime 选择不使用缓存

与 Remix 的默认功能类似,您可能希望配置路由仅在进入时或当关键加载器依赖项更改时加载。您可以通过使用 gcTime 选项与 shouldReload 选项相结合来实现此目的,shouldReload 选项接受 boolean 或一个函数,该函数接收相同的 beforeLoadloaderContext 参数,并返回一个布尔值,指示路由是否应重新加载。

tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps }) => fetchPosts(deps),
  // Do not cache this route's data after it's unloaded
  gcTime: 0,
  // Only reload the route when the user navigates to it or when deps change
  shouldReload: false,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps }) => fetchPosts(deps),
  // Do not cache this route's data after it's unloaded
  gcTime: 0,
  // Only reload the route when the user navigates to it or when deps change
  shouldReload: false,
})

在仍然预加载的同时选择不使用缓存

即使您可能选择不为路由数据使用短期缓存,您仍然可以获得预加载的好处!使用上述配置,预加载仍然可以“正常工作”,并具有默认的 preloadGcTime。这意味着,如果一个路由被预加载,然后导航到该路由,则该路由的数据将被视为新鲜,并且不会重新加载。

要选择不进行预加载,请不要通过 routerOptions.defaultPreloadrouteOptions.preload 选项启用它。

将所有 loader 事件传递到外部缓存

我们在外部数据加载页面中分解了此用例,但如果您想使用像 TanStack Query 这样的外部缓存,您可以通过将所有加载器事件传递到您的外部缓存来做到这一点。只要您使用默认值,您需要做的唯一更改是将路由器上的 defaultPreloadStaleTime 选项设置为 0

tsx
const router = createRouter({
  routeTree,
  defaultPreloadStaleTime: 0,
})
const router = createRouter({
  routeTree,
  defaultPreloadStaleTime: 0,
})

这将确保每个预加载、加载和重新加载事件都会触发您的 loader 函数,然后您的外部缓存可以处理和去重这些函数。

使用 Router 上下文

传递给 loader 函数的 context 参数是一个对象,其中包含合并的联合

  • 父路由上下文
  • 此路由的上下文,由 beforeLoad 选项提供

从路由器的最顶层开始,您可以通过 context 选项将初始上下文传递给路由器。此上下文将可用于路由器中的所有路由,并在每个路由匹配时被复制和扩展。这通过使用 beforeLoad 选项将上下文传递给路由来实现。此上下文将可用于所有路由的子路由。生成的上下文将可用于路由的 loader 函数。

在此示例中,我们将在路由上下文中创建一个函数来获取帖子,然后在我们的 loader 函数中使用它。

🧠 上下文是依赖注入的强大工具。您可以使用它将服务、hooks 和其他对象注入到您的路由器和路由中。您还可以使用路由的 beforeLoad 选项在每个路由上以累加方式向下传递数据到路由树。

  • /utils/fetchPosts.tsx
tsx
export const fetchPosts = async () => {
  const res = await fetch(`/api/posts?page=${pageIndex}`)
  if (!res.ok) throw new Error('Failed to fetch posts')
  return res.json()
}
export const fetchPosts = async () => {
  const res = await fetch(`/api/posts?page=${pageIndex}`)
  if (!res.ok) throw new Error('Failed to fetch posts')
  return res.json()
}
  • /routes/__root.tsx
tsx
import { createRootRouteWithContext } from '@tanstack/react-router'

// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
  fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
import { createRootRouteWithContext } from '@tanstack/react-router'

// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
  fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
  • /routes/posts.tsx
tsx
import { createFileRoute } from '@tanstack/react-router'

// Notice how our postsRoute references context to get our fetchPosts function
// This can be a powerful tool for dependency injection across your router
// and routes.
export const Route = createFileRoute('/posts')({
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { createFileRoute } from '@tanstack/react-router'

// Notice how our postsRoute references context to get our fetchPosts function
// This can be a powerful tool for dependency injection across your router
// and routes.
export const Route = createFileRoute('/posts')({
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
  • /router.tsx
tsx
import { routeTree } from './routeTree.gen'

// Use your routerContext to create a new router
// This will require that you fullfil the type requirements of the routerContext
const router = createRouter({
  routeTree,
  context: {
    // Supply the fetchPosts function to the router context
    fetchPosts,
  },
})
import { routeTree } from './routeTree.gen'

// Use your routerContext to create a new router
// This will require that you fullfil the type requirements of the routerContext
const router = createRouter({
  routeTree,
  context: {
    // Supply the fetchPosts function to the router context
    fetchPosts,
  },
})

使用路径参数

要在您的 loader 函数中使用路径参数,请通过函数参数上的 params 属性访问它们。这是一个示例

tsx
// routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params: { postId } }) => fetchPostById(postId),
})
// routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params: { postId } }) => fetchPostById(postId),
})

使用路由上下文

将全局上下文向下传递到您的路由器很棒,但是如果您想要提供特定于路由的上下文怎么办?这就是 beforeLoad 选项的用武之地。beforeLoad 选项是一个在尝试加载路由之前立即运行的函数,并接收与 loader 相同的参数。除了能够重定向潜在的匹配项、阻止加载器请求等之外,它还可以返回一个对象,该对象将合并到路由的上下文中。让我们看一个示例,其中我们通过 beforeLoad 选项将一些数据注入到我们的路由上下文中

tsx
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  // Pass the fetchPosts function to the route context
  beforeLoad: () => ({
    fetchPosts: () => console.info('foo'),
  }),
  loader: ({ context: { fetchPosts } }) => {
    console.info(fetchPosts()) // 'foo'

    // ...
  },
})
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  // Pass the fetchPosts function to the route context
  beforeLoad: () => ({
    fetchPosts: () => console.info('foo'),
  }),
  loader: ({ context: { fetchPosts } }) => {
    console.info(fetchPosts()) // 'foo'

    // ...
  },
})

在 Loaders 中使用搜索参数

❓ 但是等等,Tanner...我的搜索参数在哪里?!

您可能在这里想知道为什么 search 没有直接在 loader 函数的参数中提供。我们有意这样设计是为了帮助您成功。让我们看看为什么

  • 在加载器函数中使用的搜索参数非常好地表明这些搜索参数也应该用于唯一标识正在加载的数据。例如,您可能有一个路由使用像 pageIndex 这样的搜索参数,该参数唯一标识路由匹配中保存的数据。或者,想象一个 /users/user 路由,它使用搜索参数 userId 来标识应用程序中的特定用户,您可能会像这样建模您的 URL:/users/user?userId=123。这意味着您的 user 路由将需要一些额外的帮助来标识特定用户。
  • 直接在加载器函数中访问搜索参数可能会导致缓存和预加载中的错误,其中加载的数据对于当前 URL 路径名和搜索参数不是唯一的。例如,您可能会要求您的 /posts 路由预加载第 2 页的结果,但是如果在您的路由配置中没有页面的区分,您最终将在您的 /posts?page=1 屏幕上获取、存储和显示第 2 页的数据,而不是在后台预加载它!
  • 在搜索参数和加载器函数之间设置阈值允许路由器理解您的依赖项和反应性。
tsx
// /routes/users.user.tsx
export const Route = createFileRoute('/users/user')({
  validateSearch: (search) =>
    search as {
      userId: string
    },
  loaderDeps: ({ search: { userId } }) => ({
    userId,
  }),
  loader: async ({ deps: { userId } }) => getUser(userId),
})
// /routes/users.user.tsx
export const Route = createFileRoute('/users/user')({
  validateSearch: (search) =>
    search as {
      userId: string
    },
  loaderDeps: ({ search: { userId } }) => ({
    userId,
  }),
  loader: async ({ deps: { userId } }) => getUser(userId),
})

通过 routeOptions.loaderDeps 访问搜索参数

tsx
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  // Use zod to validate and parse the search params
  validateSearch: z.object({
    offset: z.number().int().nonnegative().catch(0),
  }),
  // Pass the offset to your loader deps via the loaderDeps function
  loaderDeps: ({ search: { offset } }) => ({ offset }),
  // Use the offset from context in the loader function
  loader: async ({ deps: { offset } }) =>
    fetchPosts({
      offset,
    }),
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
  // Use zod to validate and parse the search params
  validateSearch: z.object({
    offset: z.number().int().nonnegative().catch(0),
  }),
  // Pass the offset to your loader deps via the loaderDeps function
  loaderDeps: ({ search: { offset } }) => ({ offset }),
  // Use the offset from context in the loader function
  loader: async ({ deps: { offset } }) =>
    fetchPosts({
      offset,
    }),
})

使用 Abort Signal

loader 函数的 abortController 属性是一个 AbortController。当路由卸载或 loader 调用过时时,其信号将被取消。这对于在路由卸载或路由的参数更改时取消网络请求很有用。这是一个将其与 fetch 调用一起使用的示例

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: ({ abortController }) =>
    fetchPosts({
      // Pass this to an underlying fetch call or anything that supports signals
      signal: abortController.signal,
    }),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: ({ abortController }) =>
    fetchPosts({
      // Pass this to an underlying fetch call or anything that supports signals
      signal: abortController.signal,
    }),
})

使用 preload 标志

loader 函数的 preload 属性是一个布尔值,当路由正在预加载而不是加载时,该值为 true。某些数据加载库可能以不同于标准 fetch 的方式处理预加载,因此您可能希望将 preload 传递给您的数据加载库,或使用它来执行适当的数据加载逻辑

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async ({ preload }) =>
    fetchPosts({
      maxAge: preload ? 10_000 : 0, // Preloads should hang around a bit longer
    }),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async ({ preload }) =>
    fetchPosts({
      maxAge: preload ? 10_000 : 0, // Preloads should hang around a bit longer
    }),
})

处理慢速 Loaders

理想情况下,大多数路由加载器都可以在短时间内解析其数据,从而无需渲染占位符微调器,而只需依靠 suspense 在下一个路由完全准备就绪时渲染。但是,当渲染路由组件所需的关键数据速度较慢时,您有两种选择

  • 将您的快速和慢速数据拆分为单独的 promise,并在快速数据加载后 defer 慢速数据(请参阅延迟数据加载指南)。
  • 在乐观的 suspense 阈值之后显示 pending 组件,直到所有数据都准备就绪(请参阅下文)。

显示 pending 组件

默认情况下,TanStack Router 将为解析时间超过 1 秒的加载器显示 pending 组件。 这是一个乐观的阈值,可以通过以下方式配置

  • routeOptions.pendingMs
  • routerOptions.defaultPendingMs

当超过 pending 时间阈值时,路由器将渲染路由的 pendingComponent 选项(如果已配置)。

避免 Pending 组件闪烁

如果您正在使用 pending 组件,您最不希望看到的是您的 pending 时间阈值已达到,然后您的数据立即解析,从而导致 pending 组件的突兀闪烁。为了避免这种情况,默认情况下,TanStack Router 将至少显示 500 毫秒的 pending 组件。这是一个乐观的阈值,可以通过以下方式配置

  • routeOptions.pendingMinMs
  • routerOptions.defaultPendingMinMs

处理错误

TanStack Router 提供了几种处理路由加载生命周期中发生的错误的方法。让我们来看看它们。

使用 routeOptions.onError 处理错误

routeOptions.onError 选项是一个函数,当路由加载期间发生错误时调用该函数。

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  onError: ({ error }) => {
    // Log the error
    console.error(error)
  },
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  onError: ({ error }) => {
    // Log the error
    console.error(error)
  },
})

使用 routeOptions.onCatch 处理错误

routeOptions.onCatch 选项是一个函数,每当路由器的 CatchBoundary 捕获到错误时调用该函数。

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  onCatch: ({ error, errorInfo }) => {
    // Log the error
    console.error(error)
  },
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  onCatch: ({ error, errorInfo }) => {
    // Log the error
    console.error(error)
  },
})

使用 routeOptions.errorComponent 处理错误

routeOptions.errorComponent 选项是一个组件,当路由加载或渲染生命周期中发生错误时渲染该组件。它使用以下 props 渲染

  • error - 发生的错误
  • reset - 一个重置内部 CatchBoundary 的函数
tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error }) => {
    // Render an error message
    return <div>{error.message}</div>
  },
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error }) => {
    // Render an error message
    return <div>{error.message}</div>
  },
})

reset 函数可用于允许用户重试渲染错误边界的正常子项

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    return (
      <div>
        {error.message}
        <button
          onClick={() => {
            // Reset the router error boundary
            reset()
          }}
        >
          retry
        </button>
      </div>
    )
  },
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    return (
      <div>
        {error.message}
        <button
          onClick={() => {
            // Reset the router error boundary
            reset()
          }}
        >
          retry
        </button>
      </div>
    )
  },
})

如果错误是路由加载的结果,您应该改为调用 router.invalidate(),这将协调路由器重新加载和错误边界重置

tsx
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    const router = useRouter()

    return (
      <div>
        {error.message}
        <button
          onClick={() => {
            // Invalidate the route to reload the loader, which will also reset the error boundary
            router.invalidate()
          }}
        >
          retry
        </button>
      </div>
    )
  },
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    const router = useRouter()

    return (
      <div>
        {error.message}
        <button
          onClick={() => {
            // Invalidate the route to reload the loader, which will also reset the error boundary
            router.invalidate()
          }}
        >
          retry
        </button>
      </div>
    )
  },
})

使用默认的 ErrorComponent

TanStack Router 提供了一个默认的 ErrorComponent,当在路由加载或渲染生命周期中发生错误时,它会被渲染。如果您选择覆盖路由的错误组件,明智的做法仍然是始终回退到使用默认的 ErrorComponent 渲染任何未捕获的错误

tsx
// routes/posts.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error }) => {
    if (error instanceof MyCustomError) {
      // Render a custom error message
      return <div>{error.message}</div>
    }

    // Fallback to the default ErrorComponent
    return <ErrorComponent error={error} />
  },
})
// routes/posts.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error }) => {
    if (error instanceof MyCustomError) {
      // Render a custom error message
      return <div>{error.message}</div>
    }

    // Fallback to the default ErrorComponent
    return <ErrorComponent error={error} />
  },
})
订阅 Bytes

每周为您推送 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。

Bytes

无垃圾邮件。随时取消订阅。