Create your own
Lesson illustration

Preload on Hover for Faster Perceived Performance

Hello! Welcome to your tenth and final lesson in the first module of our course on mastering TanStack Start and Router.

In our last lesson, we focused on making our application more robust by handling pending states and loader errors. You learned how to provide users with clear feedback during data fetching, which is crucial for a good user experience.

Today, we're going to take performance a step further. Instead of just managing the waiting time, we'll explore how to eliminate it almost entirely in many cases.

Lesson 10: Proactive Data Fetching with Preloading

Today's Goal:

This lesson directly addresses the learning outcome: Implement data preloading on link hover to improve perceived performance.

We will explore how TanStack Router can anticipate a user's navigation and start loading the necessary data before they even click a link. This technique dramatically improves the perceived speed of your application, making navigations feel instantaneous.

What we will cover:

  1. The Concept of Preloading: Understanding what preloading is and the different strategies available.
  2. Implementation: How to enable preloading on "intent" (hover) using the Link component and global router settings.
  3. The Power of Caching: Why preloading is most effective when paired with a caching library like TanStack Query.
  4. The Full Pattern: Integrating loaders, TanStack Query, and components to achieve seamless, preloaded navigations.

1. What is Preloading?

Preloading is the act of loading a route's resources—its code and its data—before a user actually navigates to it. The router does this by predicting the user's next move. For example, when a user hovers their mouse over a link, it's a strong signal they might click it.

By fetching the data at that moment of "intent," the information can already be available in the browser's cache by the time the user clicks, making the subsequent page load feel instant.

Preloading | TanStack Router React Docs

The official documentation provides a concise introduction to preloading and the different strategies it supports. Please read the following sections.

Read the introductory 'Preloading' section and the 'Supported Preloading Strategies' section. While TanStack Router supports preloading on 'viewport' visibility and 'render', we will be focusing on the most common and impactful strategy: 'intent'.

As you've read, the 'intent' strategy (hovering or touching a link) is a powerful and low-effort way to boost your app's perceived performance.


2. Implementing Preloading on Intent

TanStack Router makes enabling preloading incredibly simple. You can do it on a per-link basis or globally for your entire application.

Per-Link Preloading

The most direct way to enable preloading is by adding the preload='intent' prop to a Link component.

Navigation | TanStack Router React Docs

The 'Navigation' documentation page shows the exact syntax for the preload prop and also introduces how to fine-tune its behavior.

Read the 'Link Preloading' and 'Link Preloading Delay' sections. Note the preload and preloadDelay props.

Here is the basic implementation:

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

function PostsList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link
            to="/posts/$postId"
            params={{ postId: post.id }}
            // Preload the route's loader on hover/touch
            preload="intent"
            // Optional: Adjust the hover delay (default is 50ms)
            preloadDelay={100}
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

Global Preloading

If you want to enable preloading by default for all links in your application, you can configure it when you create your router.

// In your src/app.tsx or wherever your router is created
import { createRouter } from '@tanstack/react-router'

const router = createRouter({
  // ... other options
  defaultPreload: 'intent',
  defaultPreloadDelay: 100, // Optional: set a global delay
})

This is a powerful "set it and forget it" optimization for your entire application.


3. The Crucial Role of Caching

Simply triggering a loader on hover is only half the story. For preloading to be effective, the fetched data must be stored somewhere. When the user finally clicks the link, the router should be able to retrieve the data from this cache instead of fetching it again.

TanStack Router has a very basic, short-term internal cache. However, for robust, configurable, and persistent caching, it's designed to integrate seamlessly with TanStack Query.

This video provides a perfect demonstration of preloading in action with TanStack Query.

SSR, Preloading, Caching and more with TanStack Start + Query

In this clip from Dev Leonardo, you'll see the practical effect of preloading. Pay close attention to the network tab when the mouse hovers over the link.

Watch from 02:52 to 05:09. The first part (until 03:51) shows the preloading implementation and result. The second part clarifies an important concept: preloading is a client-side navigation enhancement, which complements the initial server-side render you get with TanStack Start.

As you saw, the data fetch is initiated on hover, and by the time the navigation occurs, the data is already in TanStack Query's cache, resulting in an instant UI update.


4. The Complete Pattern: Preloading with TanStack Query

Let's tie everything together. The complete pattern involves three parts: the route loader, the link, and the destination component.

This video from Web Dev Cody walks through the specific code needed to connect TanStack Router's preloading with TanStack Query's caching.

Speed up your pages using prefetching (TanStack Start)

This video details the specific functions you'll use to implement the full preloading pattern.

Watch from 01:29 to 05:09. The video explains how to use context.queryClient.ensureQueryData in your loader and useSuspenseQuery (or useQuery) in your component to complete the circle.

Here's a summary of the key steps:

Step 1: Use ensureQueryData in the Loader

Your route's loader function should use ensureQueryData. This function from TanStack Query will:

  1. Check if data for the given query key is already in the cache and is fresh.
  2. If so, it returns the cached data instantly.
  3. If not, it fetches the data, adds it to the cache, and then returns it.

Step 2: Let TanStack Query Manage Freshness

To give full control over caching to TanStack Query, we should tell TanStack Router not to use its own stale-time logic. We do this by setting preloadStaleTime: 0.

Preloading | TanStack Router React Docs

The documentation explains this important configuration detail for integrating with external caching libraries.

Read the section 'Preloading with External Libraries'. It clarifies why setting defaultPreloadStaleTime: 0 is the correct approach when using TanStack Query.

Step 3: Use useSuspenseQuery or useQuery in the Component

The component for the destination route then uses useSuspenseQuery or useQuery with the exact same query options to access the data. When the component renders after a preloaded navigation, it will find the data already in the cache and display it immediately.

Example Implementation

Let's put this all together in code.

1. Define Query Options
It's a best practice to define your query options in a central place to ensure you use the same key in both the loader and the component.

// src/lib/queries/posts.ts
import { queryOptions } from '@tanstack/react-query'
import { fetchPostById } from '../server/posts' // Your server function

export const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ['posts', postId],
    queryFn: () => fetchPostById(postId),
  })

2. Configure the Route Loader
The loader will now use ensureQueryData.

// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { postQueryOptions } from '../../lib/queries/posts'

export const Route = createFileRoute('/posts/$postId')({
  // This loader runs on the server for the initial load,
  // and on the client for preloading/client-side navigation.
  loader: ({ context, params }) =>
    context.queryClient.ensureQueryData(postQueryOptions(params.postId)),
  
  // Let TanStack Query handle caching entirely
  preloadStaleTime: 0,
  
  component: PostComponent,
})

function PostComponent() {
  // ... see next step
}

3. Read Data in the Component
The component uses useSuspenseQuery to get the data. The useLoaderData hook can also be used, but using the query hook directly is common and effective.

// src/routes/posts.$postId.tsx (continued)
import { useSuspenseQuery } from '@tanstack/react-query'
import { useParams } from '@tanstack/react-router'

function PostComponent() {
  const { postId } = useParams({ from: '/posts/$postId' })
  const { data: post } = useSuspenseQuery(postQueryOptions(postId))

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  )
}

With this setup, any <Link to="/posts/$postId" ... preload="intent" /> will now trigger an incredibly fast navigation experience for your users.


Conclusion

Congratulations on completing Module 1! You've gone from initializing a project to implementing advanced performance optimizations. By preloading data, you can make your application feel significantly faster and more responsive, which is a hallmark of a high-quality web application.

Key Takeaways:

  • Preloading on 'intent' (link hover) is a powerful strategy to improve perceived performance.
  • You can enable it per-link with <Link preload="intent"> or globally with defaultPreload: 'intent' in your router configuration.
  • Preloading is most powerful when combined with a dedicated caching library like TanStack Query.
  • The standard pattern is to use queryClient.ensureQueryData in your loader and useSuspenseQuery in your component with matching query keys.
  • Set preloadStaleTime: 0 in your route options to delegate caching logic entirely to TanStack Query.

Next Module Preview:

You now have a solid foundation in the core concepts of TanStack Start and Router. In Module 2, we will shift our focus to a practical, real-world challenge: Migrating a Client-Side SPA to TanStack Start. Our first lesson will tackle a common hurdle in this process: identifying and refactoring client-only code and dependencies (like charting libraries or browser-specific APIs) to work correctly in an SSR environment.

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

Sign up