Capítulo 35 de 80

Chapter 35: Prefetching & Router Integration

Core Idea

queryClient.query() (the same method useQuery calls internally) is also the general-purpose prefetch primitive — call it ahead of when data is needed (on hover, in a parent component, inside another query's queryFn, at the router level) to flatten waterfalls without changing the eventual useQuery call.

Key Concepts

  • queryClient.query() semantics for prefetching: runs the query function, caches the result, returns it, and throws on error — respects the client's default staleTime unless you pass one explicitly; non-critical prefetches should have their promise discarded (void ...().catch(noop)) since a normal useQuery will retry anyway. Un-consumed prefetched data (no matching useQuery ever mounts) is still garbage-collected after gcTime.
  • Infinite query prefetch: queryClient.infiniteQuery({..., pages: 3}) prefetches multiple pages up front (default is just the first page) — requires getNextPageParam to walk forward.
  • Prefetch in event handlers: fire queryClient.query() from onMouseEnter/onFocus on an interactive element likely to need the data next — pair with a real staleTime so hover-prefetch isn't silently skipped as "already fresh."
  • Prefetch in components (non-Suspense): call useQuery for the child's data in the parent too, ignoring the result (optionally with notifyOnChangeProps: [] to avoid extra re-renders) — this starts the fetch immediately in parallel with the parent's own query, without hoisting the actual consumption.
  • Prefetch in components (Suspense): useSuspenseQuery can't be used to prefetch (it would block rendering); use usePrefetchQuery/usePrefetchInfiniteQuery before a <Suspense> boundary instead, optionally wrapping the "secondary" data's own suspending consumer in its own nested boundary so it doesn't block the "primary" content.
  • Prefetch inside a queryFn: when fetching A reliably means B will be needed too, kick off queryClient.query() for B from inside A's own queryFn — loads both in parallel without touching the calling component at all.
  • Conditional prefetch + code splitting: for a dependent, code-split child query, prefetching the data from inside the parent's queryFn (conditionally, based on the parent's response) loads the child's data in parallel with the child's JS chunk — the cost is including that data-fetching code in the parent's bundle instead of the lazy chunk, a tradeoff to make based on how common that code path is.
  • Router-level integration: declare each route's data dependencies ahead of time in a loader — either await critical data so the route doesn't render until it's ready (letting error boundaries catch fetch failures), or fire-and-forget secondary data so rendering isn't blocked on it. staleTime: 'static' in a loader avoids double-fetching when the data's already cached and fresh enough.
  • Manual priming: when data is already synchronously available (e.g. passed from a parent, computed locally), skip prefetching entirely and just queryClient.setQueryData(key, data) to seed the cache directly.

Code Examples

// Prefetch on hover, ahead of a click that will need the data
const prefetch = () => {
  void queryClient.query({ queryKey: ['details'], queryFn: getDetailsData, staleTime: 60000 }).catch(noop)
}
<button onMouseEnter={prefetch} onFocus={prefetch}>Show Details</button>

// Suspense-safe prefetch before a boundary
function ArticleLayout({ id }) {
  usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById })
  return <Suspense fallback="Loading article"><Article id={id} /></Suspense>
}
  • What it demonstrates: an interaction-triggered prefetch, and the Suspense-specific usePrefetchQuery pattern that avoids blocking the boundary it sits in front of.

Key Takeaways

  1. queryClient.query() is the one primitive behind every prefetch pattern in this chapter — event handlers, component-lifecycle prefetch, and inside-queryFn prefetch are all the same call in different places.
  2. Under Suspense, reach specifically for usePrefetchQuery/usePrefetchInfiniteQuery before a boundary — useQuery or useSuspenseQueries used naively for prefetching either doesn't start early enough or blocks rendering.
  3. Router-level prefetching (declaring data needs per route, in a loader) is the most systematic fix for accumulating waterfalls across a whole app — await what's critical, fire-and-forget what isn't.

Connects To

  • Performance & Request Waterfalls: the problem this chapter's every technique is a solution to.
  • Server Rendering & Hydration: router-level prefetching's server-side counterpart.
  • Suspense: the Suspense-specific prefetch hooks referenced here.