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 路由器缓存的优点
TanStack 路由器缓存的缺点
提示
如果您立即知道您希望或需要使用更强大的工具(如 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 来访问相同的钩子(以及路由对象上的其他钩子)。这应该优先于导入路由对象,因为导入路由对象可能会创建循环依赖。
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 函数访问这些搜索参数。通过明确地识别它们,/posts 的每个具有不同 offset 和 limit 的路由匹配就不会混淆!
一旦这些依赖项到位,当依赖项发生变化时,路由将始终重新加载。
// /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,路由数据将**在后台**重新加载。
要为路由禁用“陈旧时重新验证”缓存,请将 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 函数中使用它。
🧠 上下文是依赖注入的强大工具。您可以使用它将服务、钩子和其他对象注入到您的路由器和路由中。您还可以使用路由的 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。某些数据加载库处理预加载的方式可能与标准获取不同,因此您可能希望将 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 在完全准备好时渲染下一个路由。但是,当渲染路由组件所需的关键数据很慢时,您有 2 个选择:
**默认情况下,TanStack Router 将为加载时间超过 1 秒的加载器显示一个待处理组件。**这是一个乐观阈值,可以通过以下方式配置:
当待处理时间阈值被超出时,如果配置了,路由器将渲染路由的 pendingComponent 选项。
如果您正在使用待处理组件,您最不想看到的就是待处理时间阈值被达到后,数据立即解析,导致待处理组件的闪烁令人不适。为避免这种情况,**TanStack Router 默认会显示您的待处理组件至少 500 毫秒**。这是一个乐观阈值,可以通过以下方式配置:
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 选项是一个组件,当路由加载或渲染生命周期中发生错误时渲染。它使用以下 prop 渲染
// 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 万开发者。