Skip to main content
Prefetching allows you to fetch data before it’s actually needed, creating instant loading experiences for your users.

Basic Prefetching

Use queryClient.prefetchQuery() to prefetch data:
import { useQueryClient } from '@tanstack/react-query'

function Component() {
  const queryClient = useQueryClient()

  const prefetchPost = (id: number) => {
    queryClient.prefetchQuery({
      queryKey: ['post', id],
      queryFn: () => fetchPost(id),
      staleTime: 10 * 1000, // Only prefetch if data is older than 10 seconds
    })
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id} onMouseEnter={() => prefetchPost(post.id)}>
          <Link to={`/posts/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

When to Prefetch

On Hover

Prefetch when user hovers over a link:
function PostLink({ post }) {
  const queryClient = useQueryClient()

  return (
    <a
      href={`/posts/${post.id}`}
      onMouseEnter={() => {
        queryClient.prefetchQuery({
          queryKey: ['post', post.id],
          queryFn: () => fetchPost(post.id),
        })
      }}
    >
      {post.title}
    </a>
  )
}

On Focus

Prefetch when user focuses on an input:
function SearchInput() {
  const queryClient = useQueryClient()
  const [query, setQuery] = useState('')

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onFocus={() => {
        // Prefetch popular searches
        queryClient.prefetchQuery({
          queryKey: ['popular-searches'],
          queryFn: fetchPopularSearches,
        })
      }}
    />
  )
}

On Route Change

Prefetch data for the next page in pagination:
function Posts() {
  const queryClient = useQueryClient()
  const [page, setPage] = useState(0)

  const { data } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
  })

  // Prefetch next page
  useEffect(() => {
    if (data?.hasMore) {
      queryClient.prefetchQuery({
        queryKey: ['posts', page + 1],
        queryFn: () => fetchPosts(page + 1),
      })
    }
  }, [page, data, queryClient])

  return (
    <div>
      {/* Render posts */}
      <button onClick={() => setPage(page + 1)}>Next</button>
    </div>
  )
}

On Mount

Prefetch related data when component mounts:
function PostDetail({ postId }) {
  const queryClient = useQueryClient()

  const { data: post } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  // Prefetch related posts on mount
  useEffect(() => {
    if (post?.relatedIds) {
      post.relatedIds.forEach((id) => {
        queryClient.prefetchQuery({
          queryKey: ['post', id],
          queryFn: () => fetchPost(id),
        })
      })
    }
  }, [post, queryClient])

  return <div>{/* Render post */}</div>
}

Prefetch with staleTime

Prevent refetching data that’s already fresh:
queryClient.prefetchQuery({
  queryKey: ['post', id],
  queryFn: () => fetchPost(id),
  staleTime: 5 * 60 * 1000, // Only prefetch if cached data is older than 5 minutes
})
Setting staleTime in prefetch prevents unnecessary network requests if the data is already in cache and fresh.

Prefetching Infinite Queries

Prefetch the first page of an infinite query:
queryClient.prefetchInfiniteQuery({
  queryKey: ['comments', postId],
  queryFn: ({ pageParam = 0 }) => fetchComments(postId, pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  pages: 1, // Number of pages to prefetch
})

Router Integration

React Router

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

function PostsList() {
  const queryClient = useQueryClient()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link
            to={`/posts/${post.id}`}
            onMouseEnter={() => {
              queryClient.prefetchQuery({
                queryKey: ['post', post.id],
                queryFn: () => fetchPost(post.id),
              })
            }}
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

TanStack Router

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

const postRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts/$postId',
  loader: async ({ params, context }) => {
    // Prefetch in the loader
    await context.queryClient.ensureQueryData({
      queryKey: ['post', params.postId],
      queryFn: () => fetchPost(params.postId),
    })
  },
})

Next.js App Router

Prefetch in Server Components:
// app/posts/page.tsx
import { QueryClient, dehydrate, HydrationBoundary } from '@tanstack/react-query'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  // Prefetch on the server
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostsList />
    </HydrationBoundary>
  )
}

Prefetch vs ensureQueryData

prefetchQuery

Fires and forgets - doesn’t return data:
// Returns void, doesn't wait for data
queryClient.prefetchQuery({
  queryKey: ['post', id],
  queryFn: () => fetchPost(id),
})

ensureQueryData

Returns the data, waits if needed:
// Returns Promise<Data>, waits for data
const data = await queryClient.ensureQueryData({
  queryKey: ['post', id],
  queryFn: () => fetchPost(id),
})
Use ensureQueryData when you need the data immediately:
// In a router loader
loader: async ({ params, context }) => {
  const post = await context.queryClient.ensureQueryData({
    queryKey: ['post', params.postId],
    queryFn: () => fetchPost(params.postId),
  })
  
  // Can use post data here
  return { post }
}

Prefetching Multiple Queries

Prefetch several queries in parallel:
useEffect(() => {
  Promise.all([
    queryClient.prefetchQuery({
      queryKey: ['posts'],
      queryFn: fetchPosts,
    }),
    queryClient.prefetchQuery({
      queryKey: ['categories'],
      queryFn: fetchCategories,
    }),
    queryClient.prefetchQuery({
      queryKey: ['user'],
      queryFn: fetchUser,
    }),
  ])
}, [])

Conditional Prefetching

Only prefetch under certain conditions:
function PostLink({ post, prefetch = true }) {
  const queryClient = useQueryClient()

  const handleHover = () => {
    if (!prefetch) return
    
    // Only prefetch if not already in cache
    const cachedData = queryClient.getQueryData(['post', post.id])
    if (!cachedData) {
      queryClient.prefetchQuery({
        queryKey: ['post', post.id],
        queryFn: () => fetchPost(post.id),
      })
    }
  }

  return <a onMouseEnter={handleHover}>{post.title}</a>
}

Prefetching from Query Data

Use existing query data to prefetch related queries:
function usePosts() {
  const queryClient = useQueryClient()

  return useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    onSuccess: (posts) => {
      // Prefetch individual posts
      posts.forEach((post) => {
        queryClient.setQueryData(['post', post.id], post)
      })
    },
  })
}
Use setQueryData instead of prefetchQuery when you already have the data. It’s instant and doesn’t require a network request.

Background Prefetching

Prefetch data in the background while user is idle:
function useBackgroundPrefetch() {
  const queryClient = useQueryClient()

  useEffect(() => {
    const timeout = setTimeout(() => {
      // Prefetch after user has been on page for 2 seconds
      queryClient.prefetchQuery({
        queryKey: ['suggested-posts'],
        queryFn: fetchSuggestedPosts,
      })
    }, 2000)

    return () => clearTimeout(timeout)
  }, [queryClient])
}

Debounced Prefetching

Debounce prefetch calls to avoid excessive requests:
import { useDebouncedCallback } from 'use-debounce'

function SearchInput() {
  const queryClient = useQueryClient()

  const prefetchSearch = useDebouncedCallback((query: string) => {
    queryClient.prefetchQuery({
      queryKey: ['search', query],
      queryFn: () => searchPosts(query),
    })
  }, 300)

  return (
    <input
      onChange={(e) => prefetchSearch(e.target.value)}
      placeholder="Search..."
    />
  )
}

Prefetch with Custom Hook

Create a reusable prefetch hook:
function usePrefetch() {
  const queryClient = useQueryClient()

  return useCallback(
    (queryKey: QueryKey, queryFn: QueryFunction) => {
      queryClient.prefetchQuery({
        queryKey,
        queryFn,
        staleTime: 10 * 1000,
      })
    },
    [queryClient]
  )
}

// Usage
function PostsList() {
  const prefetch = usePrefetch()

  return (
    <ul>
      {posts.map((post) => (
        <li onMouseEnter={() => prefetch(['post', post.id], () => fetchPost(post.id))}>
          {post.title}
        </li>
      ))}
    </ul>
  )
}

Best Practices

  1. Set appropriate staleTime - Prevent refetching fresh data
  2. Prefetch on user intent - Hover, focus, route changes
  3. Use ensureQueryData when you need the data immediately
  4. Don’t over-prefetch - Balance UX with bandwidth usage
  5. Prefetch in parallel when possible
  6. Consider user’s connection - Maybe skip prefetch on slow connections
// Check connection before prefetching
const shouldPrefetch = navigator.connection?.effectiveType !== 'slow-2g'

if (shouldPrefetch) {
  queryClient.prefetchQuery({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id),
  })
}

Next Steps

Build docs developers (and LLMs) love