延迟数据加载

TanStack Router 的设计理念是并行运行加载器 (loader) 并等待所有加载器解析完成后再渲染下一个路由。大多数情况下这很理想,但有时您可能希望先向用户展示部分内容,同时让其他非关键数据在后台继续加载。

延迟数据加载 (Deferred data loading) 是一种模式,允许路由在渲染新位置的关键数据/标记时,让较慢的非关键路由数据在后台继续解析。该流程在客户端和服务端(通过流式传输)均可工作,是提升应用感知性能 (perceived performance) 的有效方式。

如果您使用 TanStack Query 或其他数据获取库,延迟数据加载的工作方式会有所不同。请直接跳转到 使用外部库实现延迟数据加载 章节了解更多信息。

使用 Await 实现延迟数据加载

要延迟加载较慢或非关键数据,只需在加载器响应中返回一个 未等待/未解析 的 Promise:

tsx
// src/routes/posts.$postId.tsx
import { createFileRoute, defer } from '@tanstack/solid-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async () => {
    // 获取较慢的数据但不等待
    const slowDataPromise = fetchSlowData()

    // 获取并等待快速解析的数据
    const fastData = await fetchFastData()

    return {
      fastData,
      deferredSlowData: slowDataPromise,
    }
  },
})
// src/routes/posts.$postId.tsx
import { createFileRoute, defer } from '@tanstack/solid-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async () => {
    // 获取较慢的数据但不等待
    const slowDataPromise = fetchSlowData()

    // 获取并等待快速解析的数据
    const fastData = await fetchFastData()

    return {
      fastData,
      deferredSlowData: slowDataPromise,
    }
  },
})

只要任意被等待的 Promise 解析完成,下一个路由就会开始渲染,同时延迟的 Promise 继续在后台解析。

在组件中,可以通过 Await 组件来解析和使用延迟的 Promise:

tsx
// src/routes/posts.$postId.tsx
import { createFileRoute, Await } from '@tanstack/solid-router'

export const Route = createFileRoute('/posts/$postId')({
  // ...
  component: PostIdComponent,
})

function PostIdComponent() {
  const { deferredSlowData, fastData } = Route.useLoaderData()

  // 使用 fastData 进行某些操作

  return (
    <Await promise={deferredSlowData} fallback={<div>Loading...</div>}>
      {(data) => {
        return <div>{data}</div>
      }}
    </Await>
  )
}
// src/routes/posts.$postId.tsx
import { createFileRoute, Await } from '@tanstack/solid-router'

export const Route = createFileRoute('/posts/$postId')({
  // ...
  component: PostIdComponent,
})

function PostIdComponent() {
  const { deferredSlowData, fastData } = Route.useLoaderData()

  // 使用 fastData 进行某些操作

  return (
    <Await promise={deferredSlowData} fallback={<div>Loading...</div>}>
      {(data) => {
        return <div>{data}</div>
      }}
    </Await>
  )
}

Tip

如果组件采用代码分割 (code-split),可以使用 getRouteApi 函数 来避免导入 Route 配置即可访问类型化的 useLoaderData() 钩子。

Await 组件通过触发最近的悬念边界 (suspense boundary) 来解析 Promise,解析完成后会将组件 children 作为函数渲染,并传入解析后的数据。

如果 Promise 被拒绝,Await 组件会抛出序列化的错误,该错误可以被最近的错误边界捕获。

使用外部库实现延迟数据加载

当您通过 TanStack Query 等外部库使用 外部数据加载 策略时,延迟数据加载的工作方式有所不同,因为这些库会在 TanStack Router 之外处理数据获取和缓存。

此时不需要使用 deferAwait,而应该通过路由的 loader 启动数据获取,然后在组件中使用库提供的钩子访问数据:

tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ context: { queryClient } }) => {
    // 启动较慢数据的获取但不等待
    queryClient.prefetchQuery(slowDataOptions())

    // 获取并等待快速解析的数据
    await queryClient.ensureQueryData(fastDataOptions())
  },
})
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ context: { queryClient } }) => {
    // 启动较慢数据的获取但不等待
    queryClient.prefetchQuery(slowDataOptions())

    // 获取并等待快速解析的数据
    await queryClient.ensureQueryData(fastDataOptions())
  },
})

然后在组件中使用库的钩子访问数据:

tsx
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { useSuspenseQuery } from '@tanstack/solid-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
  // ...
  component: PostIdComponent,
})

function PostIdComponent() {
  const fastData = useSuspenseQuery(fastDataOptions())

  // 使用 fastData 进行某些操作

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SlowDataComponent />
    </Suspense>
  )
}

function SlowDataComponent() {
  const data = useSuspenseQuery(slowDataOptions())

  return <div>{data}</div>
}
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { useSuspenseQuery } from '@tanstack/solid-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
  // ...
  component: PostIdComponent,
})

function PostIdComponent() {
  const fastData = useSuspenseQuery(fastDataOptions())

  // 使用 fastData 进行某些操作

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SlowDataComponent />
    </Suspense>
  )
}

function SlowDataComponent() {
  const data = useSuspenseQuery(slowDataOptions())

  return <div>{data}</div>
}

缓存与失效

流式 Promise 的生命周期与其关联的加载器数据相同,它们甚至可以被预加载!

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.