Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Route Prefetching with Tanstack Router's `preload`

Hello! Welcome back to our module on Advanced Architecture and Optimization.

In our last lesson, we explored how to apply architectural patterns inspired by React Server Components (RSCs) within Tanstack Start. We learned to use server functions (createServerFn) to isolate server-only logic, effectively reducing client bundle sizes and creating a clear separation of concerns. This laid the groundwork for efficient server-side data fetching.

Today, we'll build on that foundation to tackle a key performance optimization. This lesson addresses the learning outcome: Implement route prefetching on user intent (e.g., link hover) using Tanstack Router's preload capabilities.

We'll see how we can anticipate a user's navigation and start fetching the necessary data before they even click a link, making the application feel significantly faster.

1. The "Why" and "What" of Prefetching

In a traditional SSR navigation flow, the sequence is:

  1. User clicks a link.
  2. The browser requests the new page.
  3. The server runs the route's loader function to fetch data.
  4. The server renders the page with the data and sends it back.
  5. The user sees the new page.

The time spent in step 3 is often perceived as a delay or loading state. Prefetching aims to eliminate this perceived delay.

The core idea is to start step 3 based on the user's intent to navigate—most commonly, when they hover their mouse over a link. By the time they click, the data is already fetched and cached, making the subsequent navigation feel instantaneous.

Let's watch a quick demonstration of this effect.

Speed up your pages using prefetching (TanStack Start)

This clip from Web Dev Cody perfectly illustrates the problem of navigation latency and the dramatic improvement prefetching provides.

Watch the first 42 seconds of the video. Notice the initial delay when clicking the 'about' link, and then contrast it with the instant navigation that occurs after hovering over the link to prefetch the data.

As you saw, the user experience is vastly improved. Tanstack Router provides built-in support for this pattern, which it calls "preloading."

Preloading Strategies

Tanstack Router supports several preloading strategies, but the most common one for this use case is 'intent'. It's helpful to know the available options.

Preloading | TanStack Router React Docs

The official Tanstack Router documentation outlines the different preloading strategies available. Let's review them to understand the landscape.

Read the section 'Supported Preloading Strategies'. Pay attention to the descriptions of 'Intent', 'Viewport Visibility', and 'Render' to understand when you might use each one. We will be focusing on 'Intent' for the rest of this lesson.

2. Implementing Prefetching

You can enable prefetching in two main ways: globally for your entire application or on a per-link basis.

Method 1: Global Router Configuration

The simplest way to enable prefetching is to set it as a default in your router configuration. This is ideal when you want this behavior on most links in your app.

You can configure this in the createRouter function:

// In your router setup file (e.g., src/router.tsx)
import { createRouter } from '@tanstack/react-router'

const router = createRouter({
  // ... other router options
  defaultPreload: 'intent',
  defaultPreloadDelay: 50, // Optional: default is 50ms
})
  • defaultPreload: 'intent': This tells the router to automatically preload routes for all <Link> components on hover or touch-start.
  • defaultPreloadDelay: This is the time in milliseconds the user must maintain their intent (e.g., keep hovering) before the preload is triggered. This prevents firing off requests for every accidental mouse-over.

This configuration is detailed in the "Preloading" documentation you just reviewed.

Method 2: Per-Link Configuration

If you need more granular control, you can enable prefetching on individual <Link> components. This overrides any global default.

Navigation | TanStack Router React Docs

The 'Navigation' documentation shows how to apply preloading options directly to a <Link> component.

Read the section 'Link Preloading'. This shows the preload='intent' prop, which is the key to enabling this behavior on a specific link.

Here is the pattern in practice:

import { Link } from '@tanstack/react-router'

function NavigationMenu() {
  return (
    <nav>
      {/* This link will prefetch on hover */}
      <Link to="/posts" preload="intent">
        Blog
      </Link>

      {/* This link will use the router's default behavior */}
      <Link to="/about">
        About
      </Link>
    </nav>
  )
}

3. Prefetching with TanStack Query

When you use TanStack Query for data fetching and caching (which we've done in previous modules), the prefetching pattern becomes even more powerful. The goal is to have the router's prefetch action populate the TanStack Query cache.

The standard loader pattern involves await-ing a fetch call. However, for prefetching, we don't want the loader to block; we just want to initiate the fetch and let it run in the background.

The insureQueryData method on the queryClient is perfect for this. It checks if data for a given query key is already in the cache or is being fetched. If not, it starts fetching it.

Let's see how to wire this up.

Speed up your pages using prefetching (TanStack Start)

Returning to Web Dev Cody's video, he walks through the exact setup required to integrate router prefetching with the TanStack Query cache.

Watch the following segments carefully: Configuring Context (01:29 - 02:11): This shows a crucial one-time setup step: making the queryClient available to your route definitions via the router's context. Implementing the Loader (01:20 - 02:28): Focus on how the loader uses context.queryClient.insureQueryData. Notice it's not awaited, allowing the prefetch to happen in the background. Displaying the Data (02:28 - 03:13): See how useSuspenseQuery in the component then picks up the data that was prefetched into the cache.

Let's summarize the code pattern:

1. Expose queryClient in Router Context (One-time setup)
Your router.tsx needs to be aware of the queryClient.

// src/router.tsx
import { QueryClient } from '@tanstack/react-query'
// ...

const queryClient = new QueryClient()

export const router = createRouter({
  routeTree,
  context: {
    queryClient,
    // ... other context
  },
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

2. Use insureQueryData in the Route Loader
Modify your route's loader to use this non-blocking approach.

// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
import { aboutPageQueryOptions } from '~/lib/queries' // Your query options

export const Route = createFileRoute('/about')({
  loader: ({ context }) => {
    // This triggers the fetch but doesn't wait for it.
    // The data will be stored in the TanStack Query cache.
    context.queryClient.insureQueryData(aboutPageQueryOptions)
  },
  component: AboutComponent,
})

function AboutComponent() {
  // This hook will read from the cache. If the data is being
  // prefetched, it will suspend until it's ready. If the user
  // navigates without prefetching, it will trigger the fetch here.
  const { data } = useSuspenseQuery(aboutPageQueryOptions)

  // ... render component with data
}

This pattern elegantly handles both scenarios:

  • With prefetch: insureQueryData starts the fetch on hover. When the component renders after the click, useSuspenseQuery finds the data already in the cache (or nearly there) and renders instantly.
  • Without prefetch: The user clicks. The loader runs insureQueryData (which starts the fetch) and completes. The component renders, useSuspenseQuery sees the fetch has just started, and it suspends, showing a fallback UI until the data arrives.

4. Managing Caching Behavior

A crucial aspect of this integration is telling the router to delegate caching decisions to TanStack Query. By default, Tanstack Router has its own simple caching mechanism for preloaded data. We need to disable it to avoid conflicts.

  • defaultPreloadMaxAge: The router's built-in cache discards unused preloaded data after 30 seconds by default.
  • defaultPreloadStaleTime: The router considers its preloaded data fresh for 30 seconds by default. If another prefetch happens within this window, it won't re-run the loader.

We want TanStack Query's staleTime and cacheTime to be the source of truth. To achieve this, we tell the router that its preloaded data is always stale.

Preloading | TanStack Router React Docs

The official documentation provides a clear recommendation for integrating with external caching libraries like TanStack Query.

Read the section 'Preloading with External Libraries'. The key takeaway is setting defaultPreloadStaleTime to 0.

By setting defaultPreloadStaleTime: 0 in your router configuration, you ensure that every prefetch intent re-runs the loader. This might sound inefficient, but it's exactly what we want. When the loader runs, insureQueryData will then consult the TanStack Query cache. If the data is fresh according to Query's staleTime, it does nothing. If it's stale, it refetches. This effectively hands over all caching logic to TanStack Query.

Final Router Configuration:

// In your router setup file (e.g., src/router.tsx)
const router = createRouter({
  // ...
  context: { queryClient },
  defaultPreload: 'intent',
  defaultPreloadDelay: 50,
  // Delegate caching to TanStack Query
  defaultPreloadStaleTime: 0, 
})

Conclusion

You have now learned a powerful technique to significantly improve the perceived performance of your application. By prefetching data on user intent, you can make navigations feel seamless and instant.

Key Takeaways:

  • Prefetching on 'intent' (hover/touch) runs a route's loader in the background before a user clicks a link.
  • You can enable this globally in the router config (defaultPreload: 'intent') or on a per-link basis (preload="intent").
  • When using TanStack Query, the recommended pattern is to use context.queryClient.insureQueryData() in your loader. This populates the Query cache without blocking.
  • To correctly delegate all caching logic to TanStack Query, you must set defaultPreloadStaleTime: 0 in the router configuration.

Next Up:

We've optimized when we fetch data for a route. In the next lesson, we'll focus on optimizing the fetching process within a single route. We will explore how to execute parallel data queries inside a route loader to further reduce the time it takes to gather all the necessary data for a page.

Can't find a good explanation? Sign up and we'll make it for you

Sign up