loaderloader 参数loader 消费数据loaderDeps 访问搜索参数staleTime 控制数据被认为新鲜的时间shouldReload 和 gcTime 退出缓存routeOptions.loaderDeps 访问搜索参数preload 标志routeOptions.onError 处理错误routeOptions.onCatch 处理错误routeOptions.errorComponent 处理错误ErrorComponent数据加载是 Web 应用程序的常见关注点,与路由相关。在为您的应用程序加载页面时,理想情况下,页面的所有异步需求都应尽早并行获取和满足。路由器是协调这些异步依赖项的最佳位置,因为它通常是应用程序中唯一在内容渲染之前知道用户要去哪里的地方。
您可能熟悉 Next.js 的 getServerSideProps 或 Remix/React-Router 的 loader。TanStack Router 具有类似的功能,可以在每个路由的基础上并行预加载/加载资源,允许它在通过 suspense 获取时尽可能快地渲染。
除了路由器的这些正常期望之外,TanStack Router 更进一步,提供了内置 SWR 缓存,这是路由加载器的长期内存缓存层。这意味着您可以使用 TanStack Router 为您的路由预加载数据,使它们瞬间加载,或者临时缓存先前访问的路由的路由数据以供以后再次使用。
每次检测到 URL/历史记录更新时,路由器都会执行以下序列:
TanStack 的路由器缓存很可能适合大多数中小型应用程序,但重要的是要了解使用它与更强大的缓存解决方案(如 TanStack Query)的权衡:
TanStack Router 缓存优点:
TanStack Router 缓存缺点:
Tip
如果您立即知道您想要或需要使用更强大的东西(如 TanStack Query),请跳到外部数据加载指南。
路由器缓存是内置的,就像从任何路由的 loader 函数返回数据一样简单。让我们学习如何使用!
当加载路由匹配时,会调用路由 loader 函数。它们使用单个参数调用,该参数是包含许多有用属性的对象。我们稍后会介绍这些,但首先,让我们看一个路由 loader 函数的示例:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
loader 函数接收具有以下属性的单个对象:
使用这些参数,我们可以做很多很酷的事情,但首先,让我们看看如何控制它以及何时调用 loader 函数。
要从 loader 消费数据,请使用在 Route 对象上定义的 useLoaderData 钩子。
const posts = Route.useLoaderData()
const posts = Route.useLoaderData()
如果您无法直接访问路由对象(即您在当前路由的组件树深处),可以使用 getRouteApi 访问相同的钩子(以及 Route 对象上的其他钩子)。这应该优于导入 Route 对象,后者可能会创建循环依赖。
import { getRouteApi } from '@tanstack/react-router'
// 在您的组件中
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
import { getRouteApi } from '@tanstack/react-router'
// 在您的组件中
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
TanStack Router 为路由加载器提供了内置的陈旧时重新验证缓存层,该缓存层基于路由的依赖项进行键控:
使用这些依赖项作为键,TanStack Router 将缓存从路由的 loader 函数返回的数据,并使用它来满足对相同路由匹配的后续请求。这意味着如果路由的数据已经在缓存中,它将立即返回,然后可能根据数据的"新鲜度"在后台重新获取。
为了控制路由器依赖项和"新鲜度",TanStack Router 提供了大量选项来控制路由加载器的键控和缓存行为。让我们按照您最可能使用它们的顺序来看看:
想象一个 /posts 路由通过搜索参数 offset 和 limit 支持一些分页。为了让缓存唯一地存储这些数据,我们需要通过 loaderDeps 函数访问这些搜索参数。通过明确识别它们,具有不同 offset 和 limit 的 /posts 的每个路由匹配不会混淆!
一旦我们有了这些依赖项,当依赖项发生变化时,路由将始终重新加载。
// /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 设置为 0 毫秒(预加载为 30 秒),这意味着路由的数据将始终被视为陈旧,并且在路由匹配和导航时将始终在后台重新加载。
这对于大多数用例来说是一个很好的默认值,但您可能会发现某些路由数据更静态或可能加载成本较高。 在这些情况下,您可以使用 staleTime 选项来控制路由数据在导航时被认为新鲜的时间。让我们看一个例子:
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// 将路由数据视为新鲜 10 秒
staleTime: 10_000,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// 将路由数据视为新鲜 10 秒
staleTime: 10_000,
})
通过将 10_000 传递给 staleTime 选项,我们告诉路由器将路由的数据视为新鲜 10 秒。这意味着如果用户在最后一次加载器结果的 10 秒内从 /about 导航到 /posts,路由的数据将不会重新加载。如果用户然后在 10 秒后从 /about 导航到 /posts,路由的数据将在后台重新加载。
要为路由禁用陈旧时重新验证缓存,请将 staleTime 选项设置为 Infinity:
// /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 选项来为所有路由关闭此功能:
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
类似于 Remix 的默认功能,您可能希望配置路由仅在进入时或关键加载器依赖项更改时加载。您可以通过使用 gcTime 选项结合 shouldReload 选项来实现这一点,该选项接受 boolean 或接收相同 beforeLoad 和 loaderContext 参数并返回指示路由是否应重新加载的布尔值的函数。
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// 卸载后不缓存此路由的数据
gcTime: 0,
// 仅在用户导航到路由或依赖项更改时重新加载路由
shouldReload: false,
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// 卸载后不缓存此路由的数据
gcTime: 0,
// 仅在用户导航到路由或依赖项更改时重新加载路由
shouldReload: false,
})
即使您可能选择退出路由数据的短期缓存,您仍然可以获得预加载的好处!使用上述配置,预加载仍将使用默认的 preloadGcTime "正常工作"。这意味着如果路由被预加载,然后导航到,路由的数据将被视为新鲜,不会重新加载。
要退出预加载,请不要通过 routerOptions.defaultPreload 或 routeOptions.preload 选项启用它。
我们在外部数据加载页面中详细介绍了这个用例,但如果您想使用像 TanStack Query 这样的外部缓存,您可以通过将所有加载器事件传递给外部缓存来实现。只要您使用默认值,您需要做的唯一更改就是将路由器上的 defaultPreloadStaleTime 选项设置为 0:
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
这将确保每个预加载、加载和重新加载事件都会触发您的 loader 函数,然后可以由您的外部缓存处理和去重。
传递给 loader 函数的 context 参数是一个包含以下内容的合并联合的对象:
从路由器的最顶层开始,您可以通过 context 选项向路由器传递初始上下文。此上下文将对路由器中的所有路由可用,并在匹配时被每个路由复制和扩展。这通过 beforeLoad 选项向路由传递上下文来实现。此上下文将对路由的所有子路由可用。生成的上下文将对路由的 loader 函数可用。
在此示例中,我们将在路由上下文中创建一个函数来获取帖子,然后在我们的 loader 函数中使用它。
🧠 上下文是依赖注入的强大工具。您可以使用它将服务、钩子和其他对象注入到您的路由器和路由中。您还可以使用路由的 beforeLoad 选项在每个路由的路由树中累加传递数据。
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()
}
import { createRootRouteWithContext } from '@tanstack/react-router'
// 使用 createRootRouteWithContext<{...}>() 函数创建根路由,并传递您希望在路由器上下文中可用的任何类型。
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'
// 使用 createRootRouteWithContext<{...}>() 函数创建根路由,并传递您希望在路由器上下文中可用的任何类型。
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
import { createFileRoute } from '@tanstack/react-router'
// 注意我们的 postsRoute 如何引用上下文来获取我们的 fetchPosts 函数
// 这可以是跨路由器和路由进行依赖注入的强大工具。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { createFileRoute } from '@tanstack/react-router'
// 注意我们的 postsRoute 如何引用上下文来获取我们的 fetchPosts 函数
// 这可以是跨路由器和路由进行依赖注入的强大工具。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { routeTree } from './routeTree.gen'
// 使用您的 routerContext 创建新路由器
// 这将要求您满足 routerContext 的类型要求
const router = createRouter({
routeTree,
context: {
// 向路由器上下文提供 fetchPosts 函数
fetchPosts,
},
})
import { routeTree } from './routeTree.gen'
// 使用您的 routerContext 创建新路由器
// 这将要求您满足 routerContext 的类型要求
const router = createRouter({
routeTree,
context: {
// 向路由器上下文提供 fetchPosts 函数
fetchPosts,
},
})
要在 loader 函数中使用路径参数,请通过函数参数的 params 属性访问它们。这是一个例子:
// 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 选项向路由上下文注入一些数据的例子:
// /routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
// 将 fetchPosts 函数传递给路由上下文
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')({
// 将 fetchPosts 函数传递给路由上下文
beforeLoad: () => ({
fetchPosts: () => console.info('foo'),
}),
loader: ({ context: { fetchPosts } }) => {
console.info(fetchPosts()) // 'foo'
// ...
},
})
❓ 但是等等 Tanner... 我的搜索参数在哪里?!
您可能想知道为什么 search 在 loader 函数的参数中不能直接使用。我们有意这样设计是为了帮助您成功。让我们看看为什么:
// /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),
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
// 使用 zod 验证和解析搜索参数
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// 通过 loaderDeps 函数将 offset 传递给您的加载器依赖项
loaderDeps: ({ search: { offset } }) => ({ offset }),
// 在加载器函数中使用上下文中的 offset
loader: async ({ deps: { offset } }) =>
fetchPosts({
offset,
}),
})
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
// 使用 zod 验证和解析搜索参数
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// 通过 loaderDeps 函数将 offset 传递给您的加载器依赖项
loaderDeps: ({ search: { offset } }) => ({ offset }),
// 在加载器函数中使用上下文中的 offset
loader: async ({ deps: { offset } }) =>
fetchPosts({
offset,
}),
})
loader 函数的 abortController 属性是一个 AbortController。当路由卸载或 loader 调用过时时,其信号会被取消。这对于在路由卸载或路由参数更改时取消网络请求很有用。以下是将其与 fetch 调用一起使用的示例:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ abortController }) =>
fetchPosts({
// 将此传递给底层 fetch 调用或任何支持信号的内容
signal: abortController.signal,
}),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ abortController }) =>
fetchPosts({
// 将此传递给底层 fetch 调用或任何支持信号的内容
signal: abortController.signal,
}),
})
loader 函数的 preload 属性是一个布尔值,当路由被预加载而不是加载时为 true。一些数据加载库可能会以不同于标准 fetch 的方式处理预加载,因此您可能希望将 preload 传递给您的数据加载库,或使用它来执行适当的数据加载逻辑:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ preload }) =>
fetchPosts({
maxAge: preload ? 10_000 : 0, // 预加载应该持续更长时间
}),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ preload }) =>
fetchPosts({
maxAge: preload ? 10_000 : 0, // 预加载应该持续更长时间
}),
})
理想情况下,大多数路由加载器可以在短时间内解析其数据,无需渲染占位符旋转器,只需依靠 suspense 在完全准备好时渲染下一个路由。但是,当渲染路由组件所需的关键数据很慢时,您有 2 个选择:
默认情况下,TanStack Router 将为需要超过 1 秒才能解析的加载器显示待处理组件。 这是一个乐观的阈值,可以通过以下方式配置:
当超过待处理时间阈值时,路由器将渲染路由的 pendingComponent 选项(如果已配置)。
如果您使用待处理组件,您最不希望的是达到待处理时间阈值,然后数据立即解析,导致待处理组件出现刺眼的闪烁。为了避免这种情况,TanStack Router 默认将至少显示您的待处理组件 500 毫秒。这是一个乐观的阈值,可以通过以下方式配置:
TanStack Router 提供了几种处理路由加载生命周期中发生的错误的方法。让我们来看看它们。
routeOptions.onError 选项是一个在路由加载期间发生错误时调用的函数。
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
onError: ({ error }) => {
// 记录错误
console.error(error)
},
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
onError: ({ error }) => {
// 记录错误
console.error(error)
},
})
routeOptions.onCatch 选项是一个在路由器的 CatchBoundary 捕获错误时调用的函数。
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
onCatch: ({ error, errorInfo }) => {
// 记录错误
console.error(error)
},
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
onCatch: ({ error, errorInfo }) => {
// 记录错误
console.error(error)
},
})
routeOptions.errorComponent 选项是一个在路由加载或渲染生命周期中发生错误时渲染的组件。它使用以下属性进行渲染:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
// 渲染错误消息
return <div>{error.message}</div>
},
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
// 渲染错误消息
return <div>{error.message}</div>
},
})
reset 函数可用于允许用户重试渲染错误边界的正常子组件:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
return (
<div>
{error.message}
<button
onClick={() => {
// 重置路由器错误边界
reset()
}}
>
retry
</button>
</div>
)
},
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
return (
<div>
{error.message}
<button
onClick={() => {
// 重置路由器错误边界
reset()
}}
>
retry
</button>
</div>
)
},
})
如果错误是路由加载的结果,您应该调用 router.invalidate(),它将协调路由器重新加���和错误边界重置:
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
return (
<div>
{error.message}
<button
onClick={() => {
// 使路由无效以重新加载加载器,这也将重置错误边界
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={() => {
// 使路由无效以重新加载加载器,这也将重置错误边界
router.invalidate()
}}
>
retry
</button>
</div>
)
},
})
TanStack Router 提供了一个默认的 ErrorComponent,在路由加载或渲染生命周期中发生错误时渲染。如果您选择覆盖路由的错误组件,明智的做法是始终回退到使用默认的 ErrorComponent 渲染任何未捕获的错误:
// routes/posts.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
if (error instanceof MyCustomError) {
// 渲染自定义错误消息
return <div>{error.message}</div>
}
// 回退到默认的 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) {
// 渲染自定义错误消息
return <div>{error.message}</div>
}
// 回退到默认的 ErrorComponent
return <ErrorComponent error={error} />
},
})