重要提示
本指南面向外部状态管理库及其与 TanStack Router 的集成,用于数据获取、ssr、脱水/补水和流式传输。如果您尚未阅读标准的数据加载指南,请先阅读。
虽然 Router 非常有能力存储和管理大多数开箱即用的数据需求,但有时您可能只是想要更强大的东西!
Router 被设计为外部数据获取和缓存库的完美协调器。这意味着您可以使用任何您想要的数据获取/缓存库,路由器将协调数据的加载,使其与用户的导航和对新鲜度的期望保持一致。
任何支持异步 Promise 的数据获取库都可以与 TanStack Router 一起使用。这包括
或者,甚至...
实际上,任何可以返回 Promise 并读取/写入数据的库都可以集成。
将外部缓存/数据库集成到 Router 的最简单方法是使用 route.loader,以确保路由内部所需的数据已加载并准备好显示。
⚠️ 但为什么?在加载器中预加载关键渲染数据非常重要,原因如下:
- 没有“加载状态闪烁”
- 没有由基于组件的获取引起的水流式数据获取
- 更有利于 SEO。如果您的数据在渲染时可用,它将被搜索引擎索引。
这是一个使用 Route 的 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 的更实际的示例。
让我们看一下使用 TanStack Query 的更实际的示例。
// src/routes/posts.tsx
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
})
export const Route = createFileRoute('/posts')({
// Use the `loader` option to ensure that the data is loaded
loader: () => queryClient.ensureQueryData(postsQueryOptions),
component: () => {
// Read the data from the cache and subscribe to updates
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')({
// Use the `loader` option to ensure that the data is loaded
loader: () => queryClient.ensureQueryData(postsQueryOptions),
component: () => {
// Read the data from the cache and subscribe to updates
const {
data: { posts },
} = useSuspenseQuery(postsQueryOptions)
return (
<div>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
)
},
})
当在使用 suspense 和 Tanstack Query 时发生错误时,您需要让查询知道您希望在重新渲染时重试。这可以通过使用 useQueryErrorResetBoundary Hook 提供的 reset 函数来完成。我们可以在错误组件挂载后立即在 effect 中调用此函数。这将确保查询被重置,并在再次渲染路由组件时尝试再次获取数据。这也将涵盖用户导航离开我们的路由而不是单击 retry 按钮的情况。
export const Route = createFileRoute('/posts')({
loader: () => queryClient.ensureQueryData(postsQueryOptions),
errorComponent: ({ error, reset }) => {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
// Reset the query error boundary
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
{error.message}
<button
onClick={() => {
// Invalidate the route to reload the loader, and reset any router error boundaries
router.invalidate()
}}
>
retry
</button>
</div>
)
},
})
export const Route = createFileRoute('/posts')({
loader: () => queryClient.ensureQueryData(postsQueryOptions),
errorComponent: ({ error, reset }) => {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
// Reset the query error boundary
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
{error.message}
<button
onClick={() => {
// Invalidate the route to reload the loader, and reset any router error boundaries
router.invalidate()
}}
>
retry
</button>
</div>
)
},
})
能够与 TanStack Router 便捷的脱水/补水 API 集成的工具可以在服务器和客户端之间传输脱水数据,并在需要时对其进行补水。让我们回顾一下如何处理第三方关键数据和第三方延迟数据。
对于首次渲染/绘制所需的关键数据,TanStack Router 在配置 Router 时支持 dehydrate 和 hydrate 选项。这些回调是在路由器正常脱水和补水时在服务器和客户端上自动调用的函数,允许您使用自己的数据来扩充脱水数据。
dehydrate 函数可以返回任何可序列化的 JSON 数据,这些数据将被合并并注入到发送到客户端的脱水负载中。此负载通过 DehydrateRouter 组件传递,该组件在渲染时,会在客户端的 hydrate 函数中将数据返回给您。
例如,让我们脱水和补水一个 TanStack Query QueryClient,以便我们在服务器上获取的数据可在客户端上用于补水。
// src/router.tsx
export function createRouter() {
// Make sure you create your loader client or similar data
// stores inside of your `createRouter` function. This ensures
// that your data stores are unique to each request and
// always present on both server and client.
const queryClient = new QueryClient()
return createRouter({
routeTree,
// Optionally provide your loaderClient to the router context for
// convenience (you can provide anything you want to the router
// context!)
context: {
queryClient,
},
// On the server, dehydrate the loader client so the router
// can serialize it and send it to the client for us
dehydrate: () => {
return {
queryClientState: dehydrate(queryClient),
}
},
// On the client, hydrate the loader client with the data
// we dehydrated on the server
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
// Optionally, we can use `Wrap` to wrap our router in the loader client provider
Wrap: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})
}
// src/router.tsx
export function createRouter() {
// Make sure you create your loader client or similar data
// stores inside of your `createRouter` function. This ensures
// that your data stores are unique to each request and
// always present on both server and client.
const queryClient = new QueryClient()
return createRouter({
routeTree,
// Optionally provide your loaderClient to the router context for
// convenience (you can provide anything you want to the router
// context!)
context: {
queryClient,
},
// On the server, dehydrate the loader client so the router
// can serialize it and send it to the client for us
dehydrate: () => {
return {
queryClientState: dehydrate(queryClient),
}
},
// On the client, hydrate the loader client with the data
// we dehydrated on the server
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
// Optionally, we can use `Wrap` to wrap our router in the loader client provider
Wrap: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})
}
您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。