Important
本指南主要面向外部状态管理库及其与TanStack Router的集成,涵盖数据获取、SSR、水合/脱水及流式传输。若您尚未阅读标准的数据加载指南,请先阅读该文档。
虽然Router开箱即用就能满足大多数数据存储和管理需求,但有时您可能需要更强大的解决方案!
Router被设计为外部数据获取与缓存库的完美协调者。这意味着您可以使用任何数据获取/缓存库,而Router将以符合用户导航路径和新鲜度预期的方式协调数据加载。
任何支持异步Promise的数据获取库都能与TanStack Router配合使用,包括:
甚至包括:
实际上,任何能返回Promise并读写数据的库都可集成。
将外部缓存/数据库集成到Router中最简单的方式是通过route.loader确保路由所需数据已加载并准备就绪。
⚠️ 但为什么?在loader中预加载关键渲染数据非常重要,原因如下:
- 避免"加载状态闪烁"
- 消除由基于组件的获取引起的数据瀑布流
- 更利于SEO。若数据在渲染时可用,搜索引擎将能索引内容
以下是一个基础示例(请勿直接使用),展示如何通过路由的loader选项为数据预填充缓存:
// src/routes/posts.tsx
let postsCache = []
export const Route = createFileRoute('/posts')({
loader: async () => {
postsCache = await fetchPosts()
},
component: () => {
return (
<div>
{postsCache.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
)
},
})
// src/routes/posts.tsx
let postsCache = []
export const Route = createFileRoute('/posts')({
loader: async () => {
postsCache = await fetchPosts()
},
component: () => {
return (
<div>
{postsCache.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
)
},
})
此示例显然存在缺陷,但说明了可通过路由的loader选项为缓存预加载数据。下面我们看一个使用TanStack Query的更实际案例。
// src/routes/posts.tsx
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
})
export const Route = createFileRoute('/posts')({
// 使用`loader`选项确保数据加载
loader: () => queryClient.ensureQueryData(postsQueryOptions),
component: () => {
// 从缓存读取数据并订阅更新
const {
data: { posts },
} = useSuspenseQuery(postsQueryOptions)
return (
<div>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
)
},
})
// src/routes/posts.tsx
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
})
export const Route = createFileRoute('/posts')({
// 使用`loader`选项确保数据加载
loader: () => queryClient.ensureQueryData(postsQueryOptions),
component: () => {
// 从缓存读取数据并订阅更新
const {
data: { posts },
} = useSuspenseQuery(postsQueryOptions)
return (
<div>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
)
},
})
当使用suspense与Tanstack Query时发生错误,需要通过useQueryErrorResetBoundary钩子提供的reset函数让查询在重新渲染时重试。我们可在错误组件挂载时通过effect调用此函数。这将确保查询被重置,并在路由组件再次渲染时重新获取数据。此方案也适用于用户通过导航离开路由而非点击retry按钮的情况。
export const Route = createFileRoute('/posts')({
loader: () => queryClient.ensureQueryData(postsQueryOptions),
errorComponent: ({ error, reset }) => {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
// 重置查询错误边界
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
{error.message}
<button
onClick={() => {
// 使路由失效以重新加载loader,并重置路由错误边界
router.invalidate()
}}
>
重试
</button>
</div>
)
},
})
export const Route = createFileRoute('/posts')({
loader: () => queryClient.ensureQueryData(postsQueryOptions),
errorComponent: ({ error, reset }) => {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
// 重置查询错误边界
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
{error.message}
<button
onClick={() => {
// 使路由失效以重新加载loader,并重置路由错误边界
router.invalidate()
}}
>
重试
</button>
</div>
)
},
})
支持的工具可通过TanStack Router提供的脱水/水合API,在服务端与客户端之间传输脱水数据并在需要时重新水合。下面我们将介绍如何处理第三方关键数据和第三方延迟数据。
对于首次渲染所需的关键数据,TanStack Router在配置Router时支持**dehydrate和hydrate**选项。这些回调函数会在路由器正常脱水/水合时自动在服务端和客户端调用,允许您用自定义数据增强脱水数据。
dehydrate函数可返回任何可序列化的JSON数据,这些数据会被合并到发送至客户端的脱水载荷中。该载荷通过DehydrateRouter组件传递,当该组件渲染时,会通过客户端的hydrate函数将数据回传。
例如,让我们对TanStack Query的QueryClient进行脱水/水合操作,使得服务端获取的数据能在客户端水合:
// src/router.tsx
export function createRouter() {
// 确保在`createRouter`函数内创建loader client或类似数据存储
// 这能保证每个请求的数据存储是独立的
// 且始终存在于服务端和客户端
const queryClient = new QueryClient()
return createRouter({
routeTree,
// 可选:将loaderClient提供给路由上下文以便使用
// (您可以在路由上下文中提供任何内容!)
context: {
queryClient,
},
// 在服务端脱水loader client,以便路由器能序列化
// 并发送至客户端
dehydrate: () => {
return {
queryClientState: dehydrate(queryClient),
}
},
// 在客户端,用服务端脱水数据水合loader client
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
// 可选:使用`Wrap`将路由器包裹在loader client的Provider中
Wrap: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})
}
// src/router.tsx
export function createRouter() {
// 确保在`createRouter`函数内创建loader client或类似数据存储
// 这能保证每个请求的数据存储是独立的
// 且始终存在于服务端和客户端
const queryClient = new QueryClient()
return createRouter({
routeTree,
// 可选:将loaderClient提供给路由上下文以便使用
// (您可以在路由上下文中提供任何内容!)
context: {
queryClient,
},
// 在服务端脱水loader client,以便路由器能序列化
// 并发送至客户端
dehydrate: () => {
return {
queryClientState: dehydrate(queryClient),
}
},
// 在客户端,用服务端脱水数据水合loader client
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
// 可选:使用`Wrap`将路由器包裹在loader client的Provider中
Wrap: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})
}
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.