PatternsData Fetching Strategies
Prefetching Strategies
Load data before the user needs it for instant perceived performance
The fastest request is the one you never have to make. This guide covers smart prefetching patterns.
🎯 Goals
What we want:
✅ Zero perceived latency
✅ Instant page transitions
✅ Predictive loading
✅ Efficient network usage
✅ No wasted bandwidth🎯 Pattern 1: Hover Prefetch
Load data when user hovers over link.
import { useQueryClient } from '@tanstack/react-query';
function ProductCard({ product }: { product: Product }) {
const queryClient = useQueryClient();
const prefetchProduct = () => {
queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id),
staleTime: 60000, // Cache for 1 minute
});
};
return (
<Link
to={`/products/${product.id}`}
onMouseEnter={prefetchProduct}
onFocus={prefetchProduct} // Also prefetch on keyboard focus
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</Link>
);
}
// When user hovers, data is prefetched
// When user clicks, data is already cached!
// Result: Instant page load ⚡With Link Component
import { Link } from 'react-router-dom';
function PrefetchLink({ to, children, ...props }: LinkProps) {
const queryClient = useQueryClient();
const handleHover = () => {
// Extract route and prefetch relevant data
if (to.startsWith('/products/')) {
const id = to.split('/')[2];
queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
});
}
};
return (
<Link
to={to}
onMouseEnter={handleHover}
onFocus={handleHover}
{...props}
>
{children}
</Link>
);
}
// Usage
<PrefetchLink to="/products/123">
View Product
</PrefetchLink>Pros:
- ✅ High intent (user hovering = likely click)
- ✅ Instant perceived performance
- ✅ Easy to implement
Cons:
- ⚠️ Wasted bandwidth if user doesn't click
- ⚠️ Not mobile-friendly (no hover)
📜 Pattern 2: Viewport Prefetch
Prefetch when elements enter viewport.
import { useInView } from 'react-intersection-observer';
import { useQueryClient } from '@tanstack/react-query';
function ProductCard({ product }: { product: Product }) {
const queryClient = useQueryClient();
const { ref, inView } = useInView({
triggerOnce: true, // Only prefetch once
threshold: 0.1, // When 10% visible
});
useEffect(() => {
if (inView) {
queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id)
});
}
}, [inView, product.id, queryClient]);
return (
<div ref={ref}>
<Link to={`/products/${product.id}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
</Link>
</div>
);
}Bulk Prefetch
function ProductGrid({ products }: { products: Product[] }) {
const queryClient = useQueryClient();
const { ref, inView } = useInView();
useEffect(() => {
if (inView) {
// Prefetch all visible products
products.slice(0, 6).forEach(product => {
queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id)
});
});
}
}, [inView, products, queryClient]);
return (
<div ref={ref} className="grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Pros:
- ✅ Only prefetch what user can see
- ✅ Works on mobile
- ✅ Lower waste than hover
Cons:
- ⚠️ Still some wasted bandwidth
- ⚠️ Requires intersection observer
🔮 Pattern 3: Predictive Prefetch
Use analytics to predict next page.
interface NavigationPattern {
from: string;
to: string;
probability: number;
}
// Analytics data
const patterns: NavigationPattern[] = [
{ from: '/products', to: '/cart', probability: 0.6 },
{ from: '/cart', to: '/checkout', probability: 0.8 },
{ from: '/products/:id', to: '/products/:related', probability: 0.4 },
];
function usePredictivePrefetch(currentPath: string) {
const queryClient = useQueryClient();
useEffect(() => {
// Find likely next pages
const likely = patterns
.filter(p => p.from === currentPath && p.probability > 0.5)
.sort((a, b) => b.probability - a.probability);
// Prefetch top 2 likely pages
likely.slice(0, 2).forEach(pattern => {
prefetchRouteData(pattern.to, queryClient);
});
}, [currentPath, queryClient]);
}
function prefetchRouteData(route: string, queryClient: QueryClient) {
if (route === '/cart') {
queryClient.prefetchQuery({
queryKey: ['cart'],
queryFn: fetchCart
});
} else if (route === '/checkout') {
queryClient.prefetchQuery({
queryKey: ['checkout'],
queryFn: fetchCheckoutData
});
}
}
// Usage in app
function App() {
const location = useLocation();
usePredictivePrefetch(location.pathname);
return <Routes>...</Routes>;
}Pros:
- ✅ Data-driven prefetching
- ✅ High accuracy with good data
- ✅ Adapts to user behavior
Cons:
- ⚠️ Requires analytics
- ⚠️ Complex implementation
- ⚠️ May prefetch wrong pages
🚀 Pattern 4: Route-Based Prefetch
Prefetch data for next route on current route.
// On product list page, prefetch first product
function ProductListPage() {
const queryClient = useQueryClient();
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
useEffect(() => {
if (products && products.length > 0) {
// Prefetch first 3 products
products.slice(0, 3).forEach(product => {
queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id)
});
});
}
}, [products, queryClient]);
return <ProductList products={products} />;
}Next.js Example
// pages/products/index.tsx
export async function getStaticProps() {
const products = await fetchProducts();
// Prefetch first product data
const firstProduct = await fetchProduct(products[0].id);
return {
props: {
products,
prefetchedProduct: firstProduct
},
revalidate: 60
};
}📱 Pattern 5: Idle Prefetch
Prefetch during idle time.
function useIdlePrefetch() {
const queryClient = useQueryClient();
useEffect(() => {
// Use requestIdleCallback
const prefetch = () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Prefetch popular products
queryClient.prefetchQuery({
queryKey: ['products', 'popular'],
queryFn: fetchPopularProducts
});
// Prefetch user data
queryClient.prefetchQuery({
queryKey: ['user'],
queryFn: fetchUser
});
}, { timeout: 2000 });
} else {
// Fallback: use setTimeout
setTimeout(() => {
queryClient.prefetchQuery({
queryKey: ['products', 'popular'],
queryFn: fetchPopularProducts
});
}, 1000);
}
};
prefetch();
}, [queryClient]);
}
// Usage
function App() {
useIdlePrefetch();
return <Routes>...</Routes>;
}Pros:
- ✅ Doesn't block UI
- ✅ Uses available resources
- ✅ Good for static content
Cons:
- ⚠️ Unpredictable timing
- ⚠️ May never run on slow devices
🔗 Pattern 6: Link Prefetch (next/link style)
Prefetch when link is in viewport.
import { Link as RouterLink } from 'react-router-dom';
import { useInView } from 'react-intersection-observer';
interface PrefetchLinkProps {
to: string;
prefetch?: 'hover' | 'viewport' | 'intent';
children: React.ReactNode;
}
function PrefetchLink({
to,
prefetch = 'intent',
children
}: PrefetchLinkProps) {
const queryClient = useQueryClient();
const { ref, inView } = useInView({ triggerOnce: true });
const handlePrefetch = useCallback(() => {
// Extract data to prefetch from route
const dataKey = extractDataKey(to);
if (dataKey) {
queryClient.prefetchQuery({
queryKey: [dataKey],
queryFn: () => fetchData(dataKey)
});
}
}, [to, queryClient]);
// Viewport prefetch
useEffect(() => {
if (prefetch === 'viewport' && inView) {
handlePrefetch();
}
}, [prefetch, inView, handlePrefetch]);
// Intent prefetch (hover + small delay)
const [hovered, setHovered] = useState(false);
useEffect(() => {
if (prefetch === 'intent' && hovered) {
const timer = setTimeout(handlePrefetch, 100); // 100ms delay
return () => clearTimeout(timer);
}
}, [prefetch, hovered, handlePrefetch]);
return (
<RouterLink
ref={ref}
to={to}
onMouseEnter={prefetch === 'hover' || prefetch === 'intent'
? () => setHovered(true)
: undefined
}
onMouseLeave={prefetch === 'intent'
? () => setHovered(false)
: undefined
}
>
{children}
</RouterLink>
);
}
// Usage
<PrefetchLink to="/products/123" prefetch="hover">
View Product
</PrefetchLink>🎨 Pattern 7: Progressive Prefetch
Prefetch in priority order.
class PrefetchQueue {
private queue: Array<() => Promise<void>> = [];
private running = 0;
private maxConcurrent = 2;
add(priority: number, task: () => Promise<void>) {
this.queue.push({ priority, task });
this.queue.sort((a, b) => b.priority - a.priority);
this.process();
}
private async process() {
while (this.queue.length > 0 && this.running < this.maxConcurrent) {
const item = this.queue.shift();
if (!item) break;
this.running++;
try {
await item.task();
} catch (error) {
console.error('Prefetch failed:', error);
} finally {
this.running--;
this.process();
}
}
}
}
const prefetchQueue = new PrefetchQueue();
// Usage
function ProductList({ products }: { products: Product[] }) {
const queryClient = useQueryClient();
useEffect(() => {
products.forEach((product, index) => {
const priority = 100 - index; // First products = higher priority
prefetchQueue.add(priority, async () => {
await queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id)
});
});
});
}, [products, queryClient]);
return <div>...</div>;
}📊 Comparison Table
| Pattern | When to Use | Mobile | Accuracy | Waste |
|---|---|---|---|---|
| Hover | Desktop links | ❌ | ⭐⭐⭐⭐ | Medium |
| Viewport | Long lists | ✅ | ⭐⭐⭐ | Medium |
| Predictive | Known patterns | ✅ | ⭐⭐⭐⭐⭐ | Low |
| Route-Based | Navigation flows | ✅ | ⭐⭐⭐⭐⭐ | Low |
| Idle | Static content | ✅ | ⭐⭐⭐ | Medium |
| Intent | All links | ✅ | ⭐⭐⭐⭐ | Low |
| Progressive | Many items | ✅ | ⭐⭐⭐ | Low |
🏢 Real-World Examples
// Instant search results
// Prefetch on hover (Google Search)
// Predictive page loadAmazon
// Hover prefetch on products
// Related products prefetched
// Aggressive cart prefetchYouTube
// Prefetch next video
// Predictive based on watch history
// Thumbnail sprites prefetched📚 Key Takeaways
- Hover prefetch for desktop (80% click rate after hover)
- Viewport prefetch for mobile
- Predictive when you have data
- Route-based for known flows
- Monitor waste - Don't prefetch everything
- Respect data saver mode
- Test on slow networks - Does prefetch help or hurt?
Prefetch intelligently, not aggressively. Measure the impact on actual users.