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
- Set appropriate staleTime - Prevent refetching fresh data
- Prefetch on user intent - Hover, focus, route changes
- Use ensureQueryData when you need the data immediately
- Don’t over-prefetch - Balance UX with bandwidth usage
- Prefetch in parallel when possible
- 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