Skip to main content

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'.

Further Reading​