框架
版本

外部数据加载

重要

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

存储还是协调

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

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

支持哪些数据获取库?

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

或者,甚至...

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

使用加载器确保数据已加载

将外部缓存/数据库集成到 Router 中最简单的方法是使用route.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('/')({
  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 数据,这些数据将被合并并注入到发送到客户端的脱水负载中。

例如,让我们脱水和水合一个 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>
      )
    },
  })
}
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

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

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

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