外部数据加载

Important

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

存储还是协调

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

Router 被设计为外部数据获取和缓存库的完美协调器。这意味着您可以使用任何您想要的数据获取/缓存库,路由器将以符合用户导航和新鲜度期望的方式协调数据的加载。

支持哪些数据获取库?

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

或者,甚至...

字面上任何可以返回 Promise 并读/写数据的库都可以集成。

使用 Loader 确保数据已加载

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

⚠️ 但是为什么?在 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 或钩子

使用 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 进行错误处理

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

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

SSR 脱水/水合

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

关键脱水/水合

对于首次渲染/绘制所需的关键数据,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>
      )
    },
  })
}