Prefetching allows you to fetch and cache data before a user needs it, creating instant page transitions and eliminating loading states.
Basic Prefetching
Use prefetchQuery to load data into the cache:
import { useQueryClient } from '@tanstack/react-query'
function Todos() {
const queryClient = useQueryClient()
return (
<div>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onMouseEnter={() => {
// Prefetch the todo details on hover
queryClient.prefetchQuery({
queryKey: ['todo', todo.id],
queryFn: () => fetchTodo(todo.id)
})
}}
>
<Link to={`/todos/${todo.id}`}>
{todo.title}
</Link>
</li>
))}
</ul>
</div>
)
}
Prefetching on hover or mouseEnter provides data before the user clicks, making navigation feel instant.
prefetchQuery API
The prefetchQuery method has the same signature as fetchQuery but doesn’t return data:
prefetchQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(options: FetchQueryOptions): Promise<void>
Source: packages/query-core/src/queryClient.ts:372
Prefetch Options
Configure how prefetched data behaves:
import { useQueryClient } from '@tanstack/react-query'
function prefetchTodo(id: number) {
const queryClient = useQueryClient()
await queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
staleTime: 10 * 1000, // Consider fresh for 10 seconds
})
}
Query is fetched
Data is fetched and stored in the cache
Cache entry created
A cache entry is created with the specified options
Data becomes available
When the component mounts, data is instantly available
Prefetching in Route Loaders
Prefetch data in route loaders for instant navigation:
import { QueryClient } from '@tanstack/react-query'
import { LoaderFunctionArgs } from 'react-router-dom'
const queryClient = new QueryClient()
export const todoLoader = (queryClient: QueryClient) =>
async ({ params }: LoaderFunctionArgs) => {
const todoId = parseInt(params.todoId!)
await queryClient.prefetchQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId)
})
return null
}
// In your route config:
const router = createBrowserRouter([
{
path: '/todos/:todoId',
loader: todoLoader(queryClient),
element: <TodoDetail />
}
])
Prefetch on User Intent
Prefetch based on user behavior patterns:
function TodoList() {
const queryClient = useQueryClient()
const [focusedId, setFocusedId] = useState<number | null>(null)
useEffect(() => {
if (focusedId !== null) {
// Prefetch when user focuses on an item (keyboard navigation)
queryClient.prefetchQuery({
queryKey: ['todo', focusedId],
queryFn: () => fetchTodo(focusedId)
})
}
}, [focusedId, queryClient])
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
tabIndex={0}
onFocus={() => setFocusedId(todo.id)}
onBlur={() => setFocusedId(null)}
>
{todo.title}
</li>
))}
</ul>
)
}
Prefetch Infinite Queries
Prefetch infinite queries using prefetchInfiniteQuery:
import { useQueryClient } from '@tanstack/react-query'
function ProjectList() {
const queryClient = useQueryClient()
const prefetchProjects = () => {
queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
pages: 3 // Prefetch first 3 pages
})
}
return (
<button onMouseEnter={prefetchProjects}>
View Projects
</button>
)
}
Source: packages/query-core/src/queryClient.ts:407
Conditional Prefetching
Only prefetch when data isn’t already cached:
import { useQueryClient } from '@tanstack/react-query'
function TodoLink({ id }: { id: number }) {
const queryClient = useQueryClient()
const handlePrefetch = () => {
const cached = queryClient.getQueryData(['todo', id])
if (!cached) {
// Only prefetch if not in cache
queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id)
})
}
}
return (
<a href={`/todos/${id}`} onMouseEnter={handlePrefetch}>
View Todo
</a>
)
}
Prefetching automatically respects staleTime. If data exists and isn’t stale, it won’t refetch.
Prefetch with Dependencies
Prefetch related data together:
import { useQueryClient } from '@tanstack/react-query'
function ProjectCard({ projectId }: { projectId: number }) {
const queryClient = useQueryClient()
const prefetchProject = async () => {
// Prefetch project details
await queryClient.prefetchQuery({
queryKey: ['project', projectId],
queryFn: () => fetchProject(projectId)
})
// Prefetch project tasks
await queryClient.prefetchQuery({
queryKey: ['tasks', { projectId }],
queryFn: () => fetchProjectTasks(projectId)
})
// Prefetch project members
await queryClient.prefetchQuery({
queryKey: ['members', { projectId }],
queryFn: () => fetchProjectMembers(projectId)
})
}
return (
<div onMouseEnter={prefetchProject}>
Project #{projectId}
</div>
)
}
Experimental: Prefetch in Render
Enable prefetching during component rendering (experimental):
import { useQuery } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
// Enable globally
const queryClient = new QueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
})
// Or per query
function Todo({ id }) {
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
experimental_prefetchInRender: true
})
return <div>{data?.title}</div>
}
The experimental_prefetchInRender feature is experimental and may change in future versions. It allows queries to start fetching during render instead of waiting for useEffect.
Source: packages/query-core/src/types.ts:438
Prefetch Strategies
On Mount
Prefetch when a parent component mounts:
function Dashboard() {
const queryClient = useQueryClient()
useEffect(() => {
// Prefetch common data when dashboard loads
queryClient.prefetchQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats
})
queryClient.prefetchQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications
})
}, [queryClient])
return <div>Dashboard</div>
}
On Interaction
Prefetch on specific user interactions:
function SearchInput() {
const queryClient = useQueryClient()
const [query, setQuery] = useState('')
const handleFocus = () => {
// Prefetch popular searches when user focuses input
queryClient.prefetchQuery({
queryKey: ['popular-searches'],
queryFn: fetchPopularSearches
})
}
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={handleFocus}
/>
)
}
Prefetch next page before user reaches the end:
function InfiniteList() {
const queryClient = useQueryClient()
const [page, setPage] = useState(0)
const handleScroll = (e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight
if (bottom) {
// Prefetch next page
queryClient.prefetchQuery({
queryKey: ['items', page + 1],
queryFn: () => fetchItems(page + 1)
})
}
}
return (
<div onScroll={handleScroll}>
{/* List items */}
</div>
)
}
Best Practices
- Prefetch on hover/focus: Most effective for navigation
- Set appropriate staleTime: Avoid redundant prefetches
- Don’t over-prefetch: Balance UX with data usage
- Use with route loaders: Great for SPA navigation
- Combine with Suspense: For React 18+ streaming SSR
See Also