loader
loader
参数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 缓存的缺点
提示
如果您立即知道您想要或需要使用更强大的工具(如 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 hook。
const posts = Route.useLoaderData()
const posts = Route.useLoaderData()
如果您无法直接访问您的路由对象(即,您位于当前路由的组件树深处),则可以使用 getRouteApi 来访问相同的 hook(以及 Route 对象上的其他 hook)。这应该优先于导入 Route 对象,Route 对象很可能会创建循环依赖。
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()
TanStack Router 为路由加载器提供了一个内置的 Stale-While-Revalidate 缓存层,该缓存层基于路由的依赖项进行键控
使用这些依赖项作为键,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(),
// 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 缓存,请将 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 选项相结合来实现此目的,shouldReload 选项接受 boolean 或一个函数,该函数接收相同的 beforeLoad 和 loaderContext 参数,并返回一个布尔值,指示路由是否应重新加载。
// /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.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 函数中使用它。
🧠 上下文是依赖注入的强大工具。您可以使用它将服务、hooks 和其他对象注入到您的路由器和路由中。您还可以使用路由的 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'
// 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 ;)
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(),
})
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 属性访问它们。这是一个示例
// 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')({
// 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 函数的参数中提供。我们有意这样设计是为了帮助您成功。让我们看看为什么
// /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')({
// 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,
}),
})
loader 函数的 abortController 属性是一个 AbortController。当路由卸载或 loader 调用过时时,其信号将被取消。这对于在路由卸载或路由的参数更改时取消网络请求很有用。这是一个将其与 fetch 调用一起使用的示例
// 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,
}),
})
loader 函数的 preload 属性是一个布尔值,当路由正在预加载而不是加载时,该值为 true。某些数据加载库可能以不同于标准 fetch 的方式处理预加载,因此您可能希望将 preload 传递给您的数据加载库,或使用它来执行适当的数据加载逻辑
// 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 在下一个路由完全准备就绪时渲染。但是,当渲染路由组件所需的关键数据速度较慢时,您有两种选择
默认情况下,TanStack Router 将为解析时间超过 1 秒的加载器显示 pending 组件。 这是一个乐观的阈值,可以通过以下方式配置
当超过 pending 时间阈值时,路由器将渲染路由的 pendingComponent 选项(如果已配置)。
如果您正在使用 pending 组件,您最不希望看到的是您的 pending 时间阈值已达到,然后您的数据立即解析,从而导致 pending 组件的突兀闪烁。为了避免这种情况,默认情况下,TanStack Router 将至少显示 500 毫秒的 pending 组件。这是一个乐观的阈值,可以通过以下方式配置
TanStack Router 提供了几种处理路由加载生命周期中发生的错误的方法。让我们来看看它们。
routeOptions.onError 选项是一个函数,当路由加载期间发生错误时调用该函数。
// 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 选项是一个函数,每当路由器的 CatchBoundary 捕获到错误时调用该函数。
// 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 选项是一个组件,当路由加载或渲染生命周期中发生错误时渲染该组件。它使用以下 props 渲染
// 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 函数可用于允许用户重试渲染错误边界的正常子项
// 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(),这将协调路由器重新加载和错误边界重置
// 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>
)
},
})
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) {
// 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} />
},
})
每周为您推送 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。