重要
本指南主要面向外部状态管理库及其与 TanStack Router 在数据获取、SSR、水合/反水合和流式传输方面的集成。如果您尚未阅读标准 数据加载 指南,请先阅读。
虽然 Router 本身功能强大,可以开箱即用地存储和管理大多数数据需求,但有时您可能需要更健壮的解决方案!
Router 被设计为外部数据获取和缓存库的完美协调器。这意味着您可以使用任何您想使用的数据获取/缓存库,Router 将以符合用户导航和新鲜度期望的方式协调您数据的加载。
任何支持异步 Promise 的数据获取库都可以与 TanStack Router 一起使用。这包括:
或者,甚至...
字面上,任何可以返回 Promise 并进行数据读写的库都可以集成。
将外部缓存/数据库集成到 Router 中的最简单方法是使用 route.loaders 来确保路由内所需的数据已加载并准备好显示。
⚠️ 但为什么?出于以下几个原因,预加载关键渲染数据非常重要:
- 没有“加载闪烁”状态
- 没有由于组件式获取而导致的数据获取瀑布流
- 对 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 示例。
让我们来看看一个更真实的 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('/')({
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('/')({
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 选项。这些回调函数会在 Router 正常进行反水合和水合时在服务器和客户端上自动调用,并允许您使用自己的数据来增强反水合数据。
dehydrate 函数可以返回任何可序列化的 JSON 数据,这些数据将被合并并注入到发送到客户端的反水合负载中。
例如,让我们反水合和水合一个 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 资讯。每周一免费发送给超过 10 万开发者。