React Prefetch Data on Hover: Speed Up Your App
Prefetching loads data into cache before the user requests it, creating the illusion of instant interactions. When a user hovers over a product link, you fetch its details in the background; when they click, data is already cached. React Query makes prefetching trivial with the prefetchQuery() API and the useQuery hook's automatic stale refetching.
Prefetching is one of the highest-impact performance optimizations available to React developers. A study by Akamai (2024) shows that prefetching on hover reduces perceived latency by 200–500ms, increasing conversion rates by 15% on e-commerce sites.
Prefetching Strategies
Three primary strategies exist:
1. Hover-based: Prefetch when the user hovers over a link or product. 2. Route-based: Prefetch the next page's data when the user navigates. 3. Predictive: Use analytics or user behavior to preload data the user is likely to request next.
Hover-based is simplest and most reliable. Route-based requires integration with your router. Predictive is advanced but pays off on discovery-heavy interfaces.
Prefetch on Hover with useQueryClient
React Query's useQueryClient() hook provides prefetchQuery() to load data into cache:
import { useQueryClient } from '@tanstack/react-query';
async function fetchProduct(id) {
const response = await fetch(`/api/products/${id}`);
return response.json();
}
export function ProductLink({ productId, productName }) {
const queryClient = useQueryClient();
const handleHover = () => {
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
return (
`<a href={`/product/${productId}`} onMouseEnter={handleHover}>`
{productName}
`</a>`
);
}
When the user hovers, prefetchQuery() silently fetches the product data. When they click the link, the data is already cached and useQuery returns it instantly (or from cache with near-zero latency).
Prefetch with a Delay (Avoid Over-Fetching)
On mobile, hover doesn't exist; users tap links directly. Prefetching every link might waste bandwidth. Add a debounce:
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
export function SmartProductLink({ productId, productName }) {
const queryClient = useQueryClient();
const [hovering, setHovering] = useState(false);
const timeoutRef = useRef(null);
const handleHover = () => {
setHovering(true);
// Prefetch after 200ms of hover (not on every hover)
timeoutRef.current = setTimeout(() => {
if (hovering) {
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetch(`/api/products/${productId}`).then((r) =>
r.json()
),
staleTime: 5 * 60 * 1000,
});
}
}, 200);
};
const handleLeave = () => {
setHovering(false);
clearTimeout(timeoutRef.current);
};
return (
`<a`
href={`/product/${productId}`}
onMouseEnter={handleHover}
onMouseLeave={handleLeave}
`>`
{productName}
`</a>`
);
}
This ensures prefetch only triggers after a genuine 200ms hover, not flick-overs.
Route-Based Prefetching
Prefetch the next page's data when navigating:
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
export function NavBar() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const navigateAndPrefetch = async (path, prefetchFn) => {
// Prefetch the next page's data
await queryClient.prefetchQuery({
queryKey: ['data-for-' + path],
queryFn: prefetchFn,
});
// Then navigate
navigate(path);
};
return (
`<nav>`
`<button`
onClick={() =>
navigateAndPrefetch('/products', () =>
fetch('/api/products').then((r) => r.json())
)
}
`>`
Products
`</button>`
`</nav>`
);
}
The user sees the page load instantly because data is already cached.
Prefetching the Next Page in Paginated Lists
In paginated interfaces, prefetch the next page while viewing the current one:
import { useQuery, useQueryClient } from '@tanstack/react-query';
export function PaginatedProductList() {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const pageSize = 20;
const { data = { items: [], total: 0 } } = useQuery({
queryKey: ['products', page, pageSize],
queryFn: () => {
const offset = (page - 1) * pageSize;
return fetch(`/api/products?offset=${offset}&limit=${pageSize}`).then(
(r) => r.json()
);
},
});
const totalPages = Math.ceil(data.total / pageSize);
// Prefetch the next page when current page loads
useEffect(() => {
if (page `<` totalPages) {
const nextOffset = page * pageSize;
queryClient.prefetchQuery({
queryKey: ['products', page + 1, pageSize],
queryFn: () =>
fetch(
`/api/products?offset=${nextOffset}&limit=${pageSize}`
).then((r) => r.json()),
});
}
}, [page, totalPages, pageSize, queryClient]);
return (
`<div>`
`<ul>`
{data.items.map((item) => (
`<li key={item.id}>{item.name}`</li>`
))}
`</ul>`
`<button`
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
`>`
Next Page
`</button>`
`</div>`
);
}
When the user views page 1, you silently prefetch page 2. Clicking "Next" returns cached data instantly.
When NOT to Prefetch
Prefetching consumes bandwidth and API quota. Don't prefetch:
- For users on slow networks (check
navigator.connection.effectiveType). - If your API rate-limits requests.
- For large payloads (images, videos) unless critical.
- On routes with low click-through (low ROI).
Use the staleTime option judiciously: prefetch data becomes stale after staleTime milliseconds, triggering refetch on next use. Set it to match your cache strategy.
Key Takeaways
- Prefetch on hover reduces perceived latency by 200–500ms.
- Use
queryClient.prefetchQuery()to load data into cache silently. - Add a debounce to avoid over-fetching on mobile or flick-overs.
- Prefetch the next page in paginated lists for seamless navigation.
- Don't prefetch if it conflicts with rate limits, quota, or user bandwidth constraints.
Frequently Asked Questions
Does prefetch make a network request if data is already cached?
No. prefetchQuery() checks the cache first. If data is fresh (within staleTime), it skips the request.
Can I prefetch on visibility with IntersectionObserver?
Yes. Combine IntersectionObserver with prefetchQuery(). When an element enters the viewport, prefetch its data.
How do I track prefetch requests in DevTools?
Check the Network tab; prefetch requests appear as normal fetches. React Query DevTools also shows which queries are cached.
Should I prefetch on slow networks?
No. Use navigator.connection.effectiveType to check network speed. Skip prefetching on 'slow-2g' or '2g'.