框架
版本

数据加载

数据加载是 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

使用路由缓存还是不使用路由缓存?

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

TanStack 路由器缓存的优点

  • 内置,易于使用,无需额外依赖
  • 按路由处理去重、预加载、加载、陈旧时重新验证、后台重新获取
  • 粗粒度失效(一次性使所有路由和缓存失效)
  • 自动垃圾回收
  • 非常适用于路由之间共享数据较少的应用程序
  • SSR "开箱即用"

TanStack 路由器缓存的缺点

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

提示

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

使用路由缓存

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

路由 loaders

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

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

loader 中消费数据

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

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

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

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 Caching)

TanStack Router 为路由加载器提供了一个内置的“陈旧时重新验证”(Stale-While-Revalidate)缓存层,该缓存层以路由的依赖项为键

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

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

关键选项

为了控制路由器依赖项和“新鲜度”,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 函数访问这些搜索参数。通过明确地识别它们,/posts 的每个具有不同 offsetlimit 的路由匹配就不会混淆!

一旦这些依赖项到位,当依赖项发生变化时,路由将始终重新加载。

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,路由数据将**在后台**重新加载。

关闭陈旧时重新验证缓存

要为路由禁用“陈旧时重新验证”缓存,请将 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 选项打开它。

将所有加载器事件传递给外部缓存

我们将在外部数据加载页面详细讨论此用例,但如果您想使用 TanStack Query 等外部缓存,您可以通过将所有加载器事件传递给您的外部缓存来实现。只要您使用默认设置,唯一需要做的更改是将路由器的 defaultPreloadStaleTime 选项设置为 0

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

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

使用路由上下文

传递给 loader 函数的 context 参数是一个对象,它包含以下内容的合并联合

  • 父路由上下文
  • 此路由通过 beforeLoad 选项提供的上下文。

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

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

🧠 上下文是依赖注入的强大工具。您可以使用它将服务、钩子和其他对象注入到您的路由器和路由中。您还可以使用路由的 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'

    // ...
  },
})

在加载器中使用搜索参数

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

你可能会在这里疑惑为什么 search 没有直接在 loader 函数的参数中提供。我们特意这样设计是为了帮助你成功。让我们来看看原因

  • 在加载器函数中使用的搜索参数是一个很好的指标,表明这些搜索参数也应该用于唯一标识正在加载的数据。例如,您可能有一个路由使用像 pageIndex 这样的搜索参数,它唯一标识路由匹配中包含的数据。或者,想象一个 /users/user 路由,它使用搜索参数 userId 来标识应用程序中的特定用户,您可能会将您的 URL 建模为:/users/user?userId=123。这意味着您的 user 路由需要一些额外的帮助来识别特定用户。
  • 在加载器函数中直接访问搜索参数可能会导致缓存和预加载中的错误,因为加载的数据对于当前的 URL 路径名和搜索参数来说不是唯一的。例如,您可能会要求您的 /posts 路由预加载第二页的结果,但如果您的路由配置中没有页面区分,您最终会获取、存储并在您的 /posts?page=1 屏幕上显示第二页的数据,而不是在后台预加载!
  • 在搜索参数和加载器函数之间设置一个阈值可以使路由器理解您的依赖项和响应性。
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。某些数据加载库处理预加载的方式可能与标准获取不同,因此您可能希望将 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
    }),
})

处理慢速加载器

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

  • 将您的快速和慢速数据分成单独的 promise,并将慢速数据 defer 到快速数据加载之后(请参阅延迟数据加载指南)。
  • 在乐观的 suspense 阈值之后显示一个待处理组件,直到所有数据都准备好(参见下文)。

显示待处理组件

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

  • routeOptions.pendingMs
  • routerOptions.defaultPendingMs

当待处理时间阈值被超出时,如果配置了,路由器将渲染路由的 pendingComponent 选项。

避免待处理组件闪烁

如果您正在使用待处理组件,您最不想看到的就是待处理时间阈值被达到后,数据立即解析,导致待处理组件的闪烁令人不适。为避免这种情况,**TanStack Router 默认会显示您的待处理组件至少 500 毫秒**。这是一个乐观阈值,可以通过以下方式配置:

  • 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 选项是一个组件,当路由加载或渲染生命周期中发生错误时渲染。它使用以下 prop 渲染

  • 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} />
  },
})
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

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

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

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