框架
版本

预取与路由集成

当您知道或怀疑某些数据将需要时,您可以使用预取来提前用该数据填充缓存,从而获得更快的体验。

有几种不同的预取模式

  1. 在事件处理程序中
  2. 在组件中
  3. 通过路由集成
  4. 在服务器渲染期间(另一种路由集成形式)

在本指南中,我们将重点介绍前三种,而第四种将在 服务器渲染 & 数据水合指南高级服务器渲染指南 中进行深入介绍。

预取的一个特定用途是避免请求瀑布流,有关这些内容的深入背景和解释,请参阅 性能 & 请求瀑布流指南

prefetchQuery & prefetchInfiniteQuery

在深入探讨不同的具体预取模式之前,让我们先看一下 prefetchQueryprefetchInfiniteQuery 函数。首先是一些基础知识

  • 开箱即用,这些函数使用为 queryClient 配置的默认 staleTime 来确定缓存中的现有数据是新鲜的还是需要重新获取
  • 您也可以像这样传递一个特定的 staleTimeprefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })
    • staleTime 仅用于预取,您仍需要在任何 useQuery 调用中设置它
    • 如果您想忽略 staleTime,而是始终返回缓存中的数据,可以使用 ensureQueryData 函数。
    • 提示:如果您在服务器上预取,请为该 queryClient 设置一个高于 0 的默认 staleTime,以避免为每个预取调用传递一个特定的 staleTime
  • 如果没有任何 useQuery 实例出现预取的查询,它将在 gcTime 中指定的时间后被删除和垃圾回收
  • 这些函数返回 Promise<void>,因此从不返回查询数据。如果您需要这些,请改用 fetchQuery/fetchInfiniteQuery
  • 预取函数从不抛出错误,因为它们通常会在 useQuery 中尝试再次获取,这是一个很好的优雅回退。如果您需要捕获错误,请改用 fetchQuery/fetchInfiniteQuery

这是您使用 prefetchQuery 的方法

tsx
const prefetchTodos = async () => {
  // The results of this query will be cached like a normal query
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}
const prefetchTodos = async () => {
  // The results of this query will be cached like a normal query
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

无限查询可以像常规查询一样被预取。默认情况下,只有查询的第一页将被预取,并存储在给定的 QueryKey 下。如果您想预取超过一页,可以使用 pages 选项,在这种情况下,您还必须提供一个 getNextPageParam 函数

tsx
const prefetchProjects = async () => {
  // The results of this query will be cached like a normal query
  await queryClient.prefetchInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    pages: 3, // prefetch the first 3 pages
  })
}
const prefetchProjects = async () => {
  // The results of this query will be cached like a normal query
  await queryClient.prefetchInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    pages: 3, // prefetch the first 3 pages
  })
}

接下来,让我们看看如何在不同情况下使用这些以及其他预取方法。

在事件处理程序中预取

一种直接的预取形式是在用户与某物交互时进行。在此示例中,我们将使用 queryClient.prefetchQueryonMouseEnteronFocus 上启动预取。

tsx
function ShowDetailsButton() {
  const queryClient = useQueryClient()

  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['details'],
      queryFn: getDetailsData,
      // Prefetch only fires when data is older than the staleTime,
      // so in a case like this you definitely want to set one
      staleTime: 60000,
    })
  }

  return (
    <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
      Show Details
    </button>
  )
}
function ShowDetailsButton() {
  const queryClient = useQueryClient()

  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['details'],
      queryFn: getDetailsData,
      // Prefetch only fires when data is older than the staleTime,
      // so in a case like this you definitely want to set one
      staleTime: 60000,
    })
  }

  return (
    <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
      Show Details
    </button>
  )
}

在组件中预取

在组件生命周期中预取在您知道某个子组件或后代组件需要特定数据,但直到另一个查询加载完成后才能渲染它时非常有用。让我们借用请求瀑布流指南中的一个示例来解释

tsx
function Article({ id }) {
  const { data: articleData, isPending } = useQuery(() => {
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  if (isPending) {
    return 'Loading article...'
  }

  return (
    <>
      <ArticleHeader articleData={articleData} />
      <ArticleBody articleData={articleData} />
      <Comments id={id} />
    </>
  )
}

function Comments({ id }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  ...
}
function Article({ id }) {
  const { data: articleData, isPending } = useQuery(() => {
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  if (isPending) {
    return 'Loading article...'
  }

  return (
    <>
      <ArticleHeader articleData={articleData} />
      <ArticleBody articleData={articleData} />
      <Comments id={id} />
    </>
  )
}

function Comments({ id }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  ...
}

这将导致如下所示的请求瀑布流

1. |> getArticleById()
2.   |> getArticleCommentsById()
1. |> getArticleById()
2.   |> getArticleCommentsById()

如该指南中所述,一种方法是展平此瀑布流并提高性能,即在父组件中提升 getArticleCommentsById 查询,并将结果作为 prop 传递下去,但如果这不可行或不理想,例如当组件不相关且它们之间有多个层级时,该怎么办?

在这种情况下,我们可以改为在父组件中预取。最简单的方法是使用查询但忽略结果

tsx
function Article({ id }) {
  const { data: articleData, isPending } = useQuery(() => {
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  // Prefetch
  useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
    // Optional optimization to avoid rerenders when this query changes:
    notifyOnChangeProps: [],
  })

  if (isPending) {
    return 'Loading article...'
  }

  return (
    <>
      <ArticleHeader articleData={articleData} />
      <ArticleBody articleData={articleData} />
      <Comments id={id} />
    </>
  )
}

function Comments({ id }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  ...
}
function Article({ id }) {
  const { data: articleData, isPending } = useQuery(() => {
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  // Prefetch
  useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
    // Optional optimization to avoid rerenders when this query changes:
    notifyOnChangeProps: [],
  })

  if (isPending) {
    return 'Loading article...'
  }

  return (
    <>
      <ArticleHeader articleData={articleData} />
      <ArticleBody articleData={articleData} />
      <Comments id={id} />
    </>
  )
}

function Comments({ id }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  ...
}

这会立即开始获取 'article-comments' 并展平瀑布流

1. |> getArticleById()
1. |> getArticleCommentsById()
1. |> getArticleById()
1. |> getArticleCommentsById()

如果您想与 Suspense 一起预取,您将不得不采取不同的做法。您不能使用 useSuspenseQueries 进行预取,因为预取会阻塞组件的渲染。您也不能为预取使用 useQuery,因为这直到 Suspense 查询解析后才会开始预取。在这种情况下,您可以使用库中提供的 usePrefetchQueryusePrefetchInfiniteQuery 钩子。

您现在可以在实际需要数据的组件中使用 useSuspenseQuery。您*可能*希望将此后期组件包装在其自己的 <Suspense> 边界中,以便我们正在预取的“辅助”查询不会阻止“主要”数据的渲染。

tsx
function ArticleLayout({ id }) {
  usePrefetchQuery({
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  return (
    <Suspense fallback="Loading article">
      <Article id={id} />
    </Suspense>
  )
}

function Article({ id }) {
  const { data: articleData, isPending } = useSuspenseQuery({
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  ...
}
function ArticleLayout({ id }) {
  usePrefetchQuery({
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })

  return (
    <Suspense fallback="Loading article">
      <Article id={id} />
    </Suspense>
  )
}

function Article({ id }) {
  const { data: articleData, isPending } = useSuspenseQuery({
    queryKey: ['article', id],
    queryFn: getArticleById,
  })

  ...
}

另一种方法是在查询函数中预取。如果每次获取文章时很可能还需要评论,那么这样做是有意义的。为此,我们将使用 queryClient.prefetchQuery

tsx
const queryClient = useQueryClient()
const { data: articleData, isPending } = useQuery(() => {
  queryKey: ['article', id],
  queryFn: (...args) => {
    queryClient.prefetchQuery({
      queryKey: ['article-comments', id],
      queryFn: getArticleCommentsById,
    })

    return getArticleById(...args)
  },
})
const queryClient = useQueryClient()
const { data: articleData, isPending } = useQuery(() => {
  queryKey: ['article', id],
  queryFn: (...args) => {
    queryClient.prefetchQuery({
      queryKey: ['article-comments', id],
      queryFn: getArticleCommentsById,
    })

    return getArticleById(...args)
  },
})

在 effect 中预取也有效,但请注意,如果您在同一组件中使用 useSuspenseQuery,此 effect 要到查询完成*之后*才会运行,这可能不是您想要的。

tsx
const queryClient = useQueryClient()

useEffect(() => {
  queryClient.prefetchQuery({
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })
}, [queryClient, id])
const queryClient = useQueryClient()

useEffect(() => {
  queryClient.prefetchQuery({
    queryKey: ['article-comments', id],
    queryFn: getArticleCommentsById,
  })
}, [queryClient, id])

总而言之,如果您想在组件生命周期中预取查询,有几种不同的方法可以做到,选择最适合您情况的方法

  • 在 Suspense 边界之前预取,使用 usePrefetchQueryusePrefetchInfiniteQuery 钩子
  • 使用 useQueryuseSuspenseQueries 并忽略结果
  • 在查询函数中预取
  • 在 effect 中预取

接下来,我们来看一个稍微高级的用例。

依赖查询 & 代码分割

有时我们想根据另一个 fetch 的结果有条件地预取。考虑这个来自 性能 & 请求瀑布流指南 的示例

tsx
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))

function Feed() {
  const { data, isPending } = useQuery(() => {
    queryKey: ['feed'],
    queryFn: getFeed,
  })

  if (isPending) {
    return 'Loading feed...'
  }

  return (
    <>
      {data.map((feedItem) => {
        if (feedItem.type === 'GRAPH') {
          return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
        }

        return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
      })}
    </>
  )
}

// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['graph', feedItem.id],
    queryFn: getGraphDataById,
  })

  ...
}
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))

function Feed() {
  const { data, isPending } = useQuery(() => {
    queryKey: ['feed'],
    queryFn: getFeed,
  })

  if (isPending) {
    return 'Loading feed...'
  }

  return (
    <>
      {data.map((feedItem) => {
        if (feedItem.type === 'GRAPH') {
          return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
        }

        return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
      })}
    </>
  )
}

// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
  const { data, isPending } = useQuery(() => {
    queryKey: ['graph', feedItem.id],
    queryFn: getGraphDataById,
  })

  ...
}

如该指南所述,此示例会导致以下双重请求瀑布流

1. |> getFeed()
2.   |> JS for <GraphFeedItem>
3.     |> getGraphDataById()
1. |> getFeed()
2.   |> JS for <GraphFeedItem>
3.     |> getGraphDataById()

如果我们不能重构我们的 API,使 getFeed() 在必要时也返回 getGraphDataById() 数据,那么就无法消除 getFeed->getGraphDataById 瀑布流,但通过利用条件预取,我们至少可以并行加载代码和数据。就像上面描述的那样,有多种方法可以做到这一点,但对于这个例子,我们将在查询函数中进行

tsx
function Feed() {
  const queryClient = useQueryClient()
  const { data, isPending } = useQuery(() => {
    queryKey: ['feed'],
    queryFn: async (...args) => {
      const feed = await getFeed(...args)

      for (const feedItem of feed) {
        if (feedItem.type === 'GRAPH') {
          queryClient.prefetchQuery({
            queryKey: ['graph', feedItem.id],
            queryFn: getGraphDataById,
          })
        }
      }

      return feed
    }
  })

  ...
}
function Feed() {
  const queryClient = useQueryClient()
  const { data, isPending } = useQuery(() => {
    queryKey: ['feed'],
    queryFn: async (...args) => {
      const feed = await getFeed(...args)

      for (const feedItem of feed) {
        if (feedItem.type === 'GRAPH') {
          queryClient.prefetchQuery({
            queryKey: ['graph', feedItem.id],
            queryFn: getGraphDataById,
          })
        }
      }

      return feed
    }
  })

  ...
}

这将并行加载代码和数据

1. |> getFeed()
2.   |> JS for <GraphFeedItem>
2.   |> getGraphDataById()
1. |> getFeed()
2.   |> JS for <GraphFeedItem>
2.   |> getGraphDataById()

但是,有一个权衡,即 getGraphDataById 的代码现在包含在父 bundle 中,而不是包含在 JS for <GraphFeedItem> 中,因此您需要逐例确定最佳性能权衡。如果 GraphFeedItem 很有可能出现,那么将代码包含在父组件中可能是值得的。如果它们极其罕见,则可能不值得。

路由集成

由于组件树本身中的数据获取很容易导致请求瀑布流,并且解决这些问题的不同方法在累积到整个应用程序时可能很麻烦,因此一种有吸引力的预取方法是在路由器级别进行集成。

在这种方法中,您为每个*路由*明确声明该组件树需要哪些数据,并提前声明。由于服务器渲染传统上需要在渲染开始前加载所有数据,因此很长一段时间以来,这一直是 SSR 应用的主导方法。这仍然是一种常见的方法,您可以在 服务器渲染 & 数据水合指南 中了解更多信息。

目前,让我们专注于客户端用例,并看一个关于如何使用 Tanstack Router 实现此功能的示例。这些示例为了保持简洁省略了大量设置和样板代码,您可以在 Tanstack Router 文档 中找到一个完整的 React Query 示例。

在路由器级别进行集成时,您可以选择*阻止*该路由的渲染直到所有数据都可用,或者您可以启动预取但不等待结果。这样,您可以尽快开始渲染路由。您还可以混合这两种方法,等待一些关键数据,但开始渲染,直到所有辅助数据都完成加载。在此示例中,我们将配置一个 /article 路由,直到文章数据加载完成才渲染它,同时尽快开始预取评论,但如果评论尚未完成加载,则不会阻止路由的渲染。

tsx
const queryClient = new QueryClient()
const routerContext = new RouterContext()
const rootRoute = routerContext.createRootRoute({
  component: () => { ... }
})

const articleRoute = new Route({
  getParentRoute: () => rootRoute,
  path: 'article',
  beforeLoad: () => {
    return {
      articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle },
      commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments },
    }
  },
  loader: async ({
    context: { queryClient },
    routeContext: { articleQueryOptions, commentsQueryOptions },
  }) => {
    // Fetch comments asap, but don't block
    queryClient.prefetchQuery(commentsQueryOptions)

    // Don't render the route at all until article has been fetched
    await queryClient.prefetchQuery(articleQueryOptions)
  },
  component: ({ useRouteContext }) => {
    const { articleQueryOptions, commentsQueryOptions } = useRouteContext()
    const articleQuery = useQuery(() => articleQueryOptions)
    const commentsQuery = useQuery(() => commentsQueryOptions)

    return (
      ...
    )
  },
  errorComponent: () => 'Oh crap!',
})
const queryClient = new QueryClient()
const routerContext = new RouterContext()
const rootRoute = routerContext.createRootRoute({
  component: () => { ... }
})

const articleRoute = new Route({
  getParentRoute: () => rootRoute,
  path: 'article',
  beforeLoad: () => {
    return {
      articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle },
      commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments },
    }
  },
  loader: async ({
    context: { queryClient },
    routeContext: { articleQueryOptions, commentsQueryOptions },
  }) => {
    // Fetch comments asap, but don't block
    queryClient.prefetchQuery(commentsQueryOptions)

    // Don't render the route at all until article has been fetched
    await queryClient.prefetchQuery(articleQueryOptions)
  },
  component: ({ useRouteContext }) => {
    const { articleQueryOptions, commentsQueryOptions } = useRouteContext()
    const articleQuery = useQuery(() => articleQueryOptions)
    const commentsQuery = useQuery(() => commentsQueryOptions)

    return (
      ...
    )
  },
  errorComponent: () => 'Oh crap!',
})

与其他路由器的集成也是可能的,请参阅 react-router 以获取另一个演示。

手动预填查询

如果您已经同步获得了查询数据,则无需预取。您可以使用 Query Client 的 setQueryData 方法,通过 key 直接添加或更新查询的缓存结果。

tsx
queryClient.setQueryData(['todos'], todos)
queryClient.setQueryData(['todos'], todos)

延伸阅读

要深入了解如何在获取数据之前将其放入 Query Cache,请查看社区资源中的 #17: Seeding the Query Cache

与服务器端路由器和框架集成与我们刚才看到的非常相似,只是数据必须从服务器传递到客户端,以便在那里进行数据水合。要了解如何操作,请继续阅读 服务器渲染 & 数据水合指南