框架
版本

外部数据加载

重要提示

本指南面向外部状态管理库及其与 TanStack Router 的集成,用于数据获取、ssr、水合/反脱水和流式处理。如果您尚未阅读标准的 数据加载 指南,请先阅读。

存储还是协调

虽然 Router 非常有能力开箱即用地存储和管理大多数数据需求,但有时您可能只是想要更强大的功能!

Router 被设计为外部数据获取和缓存库的完美协调器。这意味着您可以使用任何您想要的数据获取/缓存库,并且路由器将协调数据的加载,使其与用户的导航和对新鲜度的期望保持一致。

支持哪些数据获取库?

任何支持异步 Promise 的数据获取库都可以与 TanStack Router 一起使用。这包括

或者,甚至...

实际上,任何可以返回 Promise 并读取/写入数据的库都可以集成。

使用 Loaders 确保数据已加载

将外部缓存/数据库集成到 Router 中最简单的方法是使用 route.loaders 以确保路由内部所需的数据已加载并准备好显示。

⚠️ 但为什么?在 loader 中预加载关键渲染数据非常重要,原因如下

  • 没有加载状态的闪烁
  • 没有瀑布式数据获取,由基于组件的获取引起
  • 更有利于 SEO。如果您的数据在渲染时可用,它将被搜索引擎索引。

这是一个使用路由的 loader 选项来为某些数据播种缓存的简单示例(不要这样做)

tsx
// 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。

  • fetchPosts 替换为您首选的数据获取库的预获取 API
  • postsCache 替换为您首选的数据获取库的读取或获取 API 或 hook

一个更实际的例子,使用 TanStack Query

让我们看一个更实际的例子,使用 TanStack Query。

tsx
// 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>
    )
  },
})

使用 TanStack Query 进行错误处理

当在使用 suspenseTanstack Query 时发生错误时,您需要让查询知道您想在重新渲染时再次尝试。这可以通过使用 useQueryErrorResetBoundary hook 提供的 reset 函数来完成。我们可以在错误组件挂载后立即在 effect 中调用此函数。这将确保查询被重置,并在再次渲染路由组件时尝试再次获取数据。这也将涵盖用户导航离开我们的路由而不是单击 retry 按钮的情况。

tsx
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>
    )
  },
})

SSR 反脱水/水合

能够集成的工具可以使用 TanStack Router 便捷的反脱水/水合 APIs 在服务器和客户端之间传输脱水数据,并在需要时重新水合它。让我们来了解如何使用第三方关键数据和第三方延迟数据来做到这一点。

关键反脱水/水合

对于首次渲染/绘制所需的关键数据,TanStack Router 在配置 Router 时支持 dehydratehydrate 选项。这些回调是在路由器正常反脱水和水合时在服务器和客户端上自动调用的函数,允许您使用自己的数据扩充反脱水数据。

dehydrate 函数可以返回任何可序列化的 JSON 数据,这些数据将被合并并注入到发送到客户端的反脱水有效负载中。此有效负载通过 DehydrateRouter 组件传递,该组件在渲染时,会将数据返回给客户端上的 hydrate 函数。

例如,让我们反脱水和水合一个 TanStack Query QueryClient,以便我们在服务器上获取的数据可以在客户端上用于水合。

tsx
// 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>
      )
    },
  })
}
订阅 Bytes

您的每周 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。

Bytes

无垃圾邮件。随时取消订阅。