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路由缓存的缺点:
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/solid-router'
// 在您的组件中
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
import { getRouteApi } from '@tanstack/solid-router'
// 在您的组件中
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(),
// 路由数据被视为新鲜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选项来实现这一点,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/solid-router'
// 使用createRootRouteWithContext<{...}>()函数创建根路由,并传递您希望在路由器上下文中可用的任何类型。
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // 注意:双调用是有意的,因为createRootRouteWithContext是一个工厂函数;)
import { createRootRouteWithContext } from '@tanstack/solid-router'
// 使用createRootRouteWithContext<{...}>()函数创建根路由,并传递您希望在路由器上下文中可用的任何类型。
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // 注意:双调用是有意的,因为createRootRouteWithContext是一个工厂函数;)
import { createFileRoute } from '@tanstack/solid-router'
// 注意我们的postsRoute如何引用上下文以获取fetchPosts函数
// 这可以成为在路由器和路由之间进行依赖注入的强大工具。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { createFileRoute } from '@tanstack/solid-router'
// 注意我们的postsRoute如何引用上下文以获取fetchPosts函数
// 这可以成为在路由器和路由之间进行依赖注入的强大工具。
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
import { routeTree } from './routeTree.gen'
```以下是翻译后的中文文档,保持所有代码块、Markdown格式、HTML标签和变量不变:
// 使用你的routerContext创建一个新路由
// 这将要求你满足routerContext的类型要求
const router = createRouter({
routeTree,
context: {
// 将fetchPosts函数提供给路由上下文
fetchPosts,
},
})
import { routeTree } from './routeTree.gen'
```以下是翻译后的中文文档,保持所有代码块、Markdown格式、HTML标签和变量不变:
// 使用你的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/solid-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/solid-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的对象
signal: abortController.signal,
}),
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ abortController }) =>
fetchPosts({
// 将此传递给底层fetch调用或任何支持signal的对象
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, // 预加载的数据应保留更久
}),
})
理想情况下,大多数路由加载器都能在短时间内解析其数据,无需渲染占位符spinner,只需依赖suspense在路由完全准备好时渲染下一个路由。但当渲染路由组件所需的关键数据较慢时,你有两个选择:
默认情况下,TanStack Router会在加载器解析时间超过1秒时显示待处理组件。 这是一个乐观阈值,可通过以下方式配置:
当超过待处理时间阈值时,如果配置了pendingComponent选项,路由将渲染该组件。
如果使用待处理组件,最不希望看到的是待处理时间阈值刚满足,数据就立即解析,导致待处理组件出现刺眼的闪烁。为避免这种情况,TanStack Router默认会显示待处理组件至少500ms。这是一个乐观阈值,可通过以下方式配置:
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选项是一个组件,当路由加载或渲染生命周期中发生错误时渲染。它接收以下props:
// 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()
}}
>
重试
</button>
</div>
)
},
})
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
return (
<div>
{error.message}
<button
onClick={() => {
// 重置路由错误边界
reset()
}}
>
重试
</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()
}}
>
重试
</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()
}}
>
重试
</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) {
// 渲染自定义错误信息
return <div>{error.message}</div>
}
// 回退到默认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) {
// 渲染自定义错误信息
return <div>{error.message}</div>
}
// 回退到默认ErrorComponent
return <ErrorComponent error={error} />
},
})
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.