数据加载

数据加载是 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 Router 缓存优点

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

TanStack Router 缓存缺点

  • 无持久化适配器/模型
  • 路由之间无共享缓存/去重
  • 无内置 mutation API(许多示例中提供了一个基本的 useMutation hook,对于许多用例来说可能足够了)
  • 无内置缓存级别乐观更新 API(您仍然可以使用 useMutation hook 等工具在组件级别实现此目的)

提示

如果您知道需要或想要使用 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 函数。

loaders 中消费数据

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

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

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

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

// in your component

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

// in your component

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

基于依赖的 stale-while-revalidate 缓存

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

  • 路由完全解析的路径名
    • 例如 /posts/1 vs /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 秒内将路由数据视为新鲜。这意味着,如果用户在上次 loader 结果后的 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 的默认功能类似,您可能希望配置一个路由,使其仅在进入时或关键 loader 依赖项更改时加载。您可以通过结合使用 gcTime 选项和 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 等外部缓存,您可以通过将所有 loader 事件传递给您的外部缓存来实现。只要您使用默认值,您需要做的唯一更改就是将路由器上的 defaultPreloadStaleTime 选项设置为 0

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

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

使用路由上下文

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

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

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

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

🧠 上下文是依赖注入的强大工具。您可以使用它将服务、hook 和其他对象注入到您的路由器和路由中。您还可以通过路由的 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/solid-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/solid-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/solid-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/solid-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 相同的参数。除了能够重定向潜在匹配、阻止 loader 请求等功能外,它还可以返回一个将被合并到路由上下文中的对象。让我们看一个示例,其中我们通过 beforeLoad 选项将一些数据注入到路由上下文中:

tsx
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/solid-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/solid-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 函数的参数中可用。我们特意这样设计是为了帮助您成功。让我们看看原因:

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

处理慢速 Loader

理想情况下,大多数路由加载器可以在很短的时间内解决它们的数据,从而无需渲染占位符加载器,并只需依赖 suspense 在数据完全准备好后渲染下一个路由。但是,当渲染路由组件所需的数据很慢时,您有两种选择:

  • 将快速数据和慢速数据拆分为不同的 promises,并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 选项是一个在路由加载或渲染生命周期中发生错误时被渲染的组件。它会以以下 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/solid-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/solid-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

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