loader
sloader
参数loader
s 中消费数据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(以及路由对象上的其他 hook)。这应该优于导入路由对象,因为后者很可能创建循环依赖。
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()
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 秒内将路由数据视为新鲜。这意味着,如果用户在上次 loader 结果后的 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 的默认功能类似,您可能希望配置一个路由,使其仅在进入时或关键 loader 依赖项更改时加载。您可以通过结合使用 gcTime 选项和 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 等外部缓存,您可以通过将所有 loader 事件传递给您的外部缓存来实现。只要您使用默认值,您需要做的唯一更改就是将路由器上的 defaultPreloadStaleTime 选项设置为 0
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
这将确保每次预加载、加载和重新加载事件都会触发您的 loader 函数,这些函数随后可以由您的外部缓存处理和去重。
传递给 loader 函数的 context 参数是一个对象,其中包含以下内容的合并联合:
从路由器的最顶层开始,您可以通过 context 选项传递初始上下文。此上下文将可用于路由器中的所有路由,并将在匹配每个路由时进行复制和扩展。这是通过 beforeLoad 选项将上下文传递给路由来完成的。此上下文将可用于路由的所有子路由。生成的上下文将可用于路由的 loader 函数。
在此示例中,我们将在路由上下文中创建一个函数来获取帖子,然后在我们的 loader 函数中使用它。
🧠 上下文是依赖注入的强大工具。您可以使用它将服务、hook 和其他对象注入到您的路由器和路由中。您还可以通过路由的 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/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 ;)
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(),
})
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 相同的参数。除了能够重定向潜在匹配、阻止 loader 请求等功能外,它还可以返回一个将被合并到路由上下文中的对象。让我们看一个示例,其中我们通过 beforeLoad 选项将一些数据注入到路由上下文中:
// /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 函数的参数中可用。我们特意这样设计是为了帮助您成功。让我们看看原因:
// /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 秒的加载器显示等待组件。这是一个乐观阈值,可以通过以下方式配置:
当超过等待时间阈值时,路由器将渲染路由的 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
选项是一个在路由加载或渲染生命周期中发生错误时被渲染的组件。它会以以下 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/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} />
},
})
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。