PatternsData Fetching Strategies
Request Deduplication
Prevent duplicate API requests for the same data
Multiple components requesting the same data at the same time can overload servers. This guide shows how to deduplicate requests.
🎯 The Problem
// Header needs user
function Header() {
const { data } = useQuery('/api/user');
return <div>{data.name}</div>;
}
// Sidebar needs user
function Sidebar() {
const { data } = useQuery('/api/user');
return <div>{data.email}</div>;
}
// Profile needs user
function Profile() {
const { data } = useQuery('/api/user');
return <div>{data.bio}</div>;
}
// Result: 3 identical API calls! ❌
// We should only make 1 call ✅🔄 Pattern 1: Request Pooling
Share in-flight requests.
class RequestPool {
private pending = new Map<string, Promise<any>>();
async fetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
// Check if request is already in flight
if (this.pending.has(key)) {
console.log(`Deduplicating request: ${key}`);
return this.pending.get(key)!;
}
// Start new request
const promise = fetcher().finally(() => {
// Clean up after request completes
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
}
clear(): void {
this.pending.clear();
}
}
export const requestPool = new RequestPool();Usage
// API wrapper
async function fetchUser(): Promise<User> {
return requestPool.fetch('user', async () => {
const response = await fetch('/api/user');
return response.json();
});
}
// All three components call fetchUser()
// Only 1 actual HTTP request is made!⚡ Pattern 2: React Query (Automatic Deduplication)
React Query handles this automatically.
import { useQuery } from '@tanstack/react-query';
// Header
function Header() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return <div>{data?.name}</div>;
}
// Sidebar
function Sidebar() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return <div>{data?.email}</div>;
}
// Profile
function Profile() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return <div>{data?.bio}</div>;
}
// All three hooks share the same query!
// Only 1 HTTP request made ✅With Parameters
// Multiple components need same product
function ProductImage({ id }: { id: string }) {
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
});
return <img src={data?.image} />;
}
function ProductPrice({ id }: { id: string }) {
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
});
return <span>${data?.price}</span>;
}
function ProductName({ id }: { id: string }) {
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
});
return <h2>{data?.name}</h2>;
}
// Same queryKey = shared request ✅🎯 Pattern 3: SWR (Built-in Deduplication)
SWR also deduplicates automatically.
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
// Header
function Header() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data?.name}</div>;
}
// Sidebar
function Sidebar() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data?.email}</div>;
}
// Profile
function Profile() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data?.bio}</div>;
}
// Same key = deduplicated! ✅Configure Deduplication Window
import { SWRConfig } from 'swr';
function App() {
return (
<SWRConfig value={{
dedupingInterval: 2000, // Dedupe requests within 2 seconds
fetcher: (url) => fetch(url).then(r => r.json())
}}>
<YourApp />
</SWRConfig>
);
}🔧 Pattern 4: Custom Hook with Deduplication
Build your own deduplicated fetcher.
import { useState, useEffect, useRef } from 'react';
// Global request cache
const requestCache = new Map<string, {
promise: Promise<any>;
data: any;
error: any;
}>();
export function useDedupedFetch<T>(
key: string,
fetcher: () => Promise<T>
) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
// Check if request already exists
if (requestCache.has(key)) {
const cached = requestCache.get(key)!;
if (cached.data) {
setData(cached.data);
setLoading(false);
return;
}
// Wait for in-flight request
const result = await cached.promise;
if (!cancelled) {
setData(result);
setLoading(false);
}
return;
}
// Start new request
const promise = fetcher();
requestCache.set(key, { promise, data: null, error: null });
const result = await promise;
if (!cancelled) {
setData(result);
setLoading(false);
// Update cache
requestCache.set(key, {
promise,
data: result,
error: null
});
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [key]);
return { data, error, loading };
}Usage
function Header() {
const { data } = useDedupedFetch('user', fetchUser);
return <div>{data?.name}</div>;
}
function Sidebar() {
const { data } = useDedupedFetch('user', fetchUser);
return <div>{data?.email}</div>;
}
// Automatically deduplicated!⏱️ Pattern 5: Time-Based Deduplication
Deduplicate requests within a time window.
class TimedDeduplicator {
private cache = new Map<string, {
promise: Promise<any>;
timestamp: number;
}>();
private window: number; // milliseconds
constructor(windowMs: number = 1000) {
this.window = windowMs;
}
async fetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const now = Date.now();
const cached = this.cache.get(key);
// Reuse if within time window
if (cached && (now - cached.timestamp) < this.window) {
console.log(`Deduplicating (${now - cached.timestamp}ms old)`);
return cached.promise;
}
// Start new request
const promise = fetcher();
this.cache.set(key, { promise, timestamp: now });
// Clean up after completion
promise.finally(() => {
setTimeout(() => {
this.cache.delete(key);
}, this.window);
});
return promise;
}
}
const deduplicator = new TimedDeduplicator(2000); // 2 second window
// Usage
async function fetchUser(): Promise<User> {
return deduplicator.fetch('user', async () => {
const response = await fetch('/api/user');
return response.json();
});
}
// Requests within 2 seconds are deduplicated🎨 Pattern 6: GraphQL DataLoader
Batch and deduplicate GraphQL queries.
import DataLoader from 'dataloader';
// Create a DataLoader for users
const userLoader = new DataLoader<string, User>(async (ids) => {
// Batch load all users at once
console.log(`Batching ${ids.length} user requests`);
const response = await fetch('/graphql', {
method: 'POST',
body: JSON.stringify({
query: `
query GetUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
email
}
}
`,
variables: { ids }
})
});
const { data } = await response.json();
// Return users in same order as ids
return ids.map(id => data.users.find((u: User) => u.id === id));
});
// Usage
async function getUser(id: string): Promise<User> {
return userLoader.load(id);
}
// These three calls are batched into ONE GraphQL query!
const user1 = await getUser('1');
const user2 = await getUser('2');
const user3 = await getUser('3');With React
import { useEffect, useState } from 'react';
function UserProfile({ id }: { id: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Automatically batched!
userLoader.load(id).then(setUser);
}, [id]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
// Render multiple profiles
<UserProfile id="1" />
<UserProfile id="2" />
<UserProfile id="3" />
// Only 1 GraphQL request made! ✅🔍 Pattern 7: Debounced Deduplication
Deduplicate rapid successive calls.
class DebouncedDeduplicator {
private pending = new Map<string, {
timer: NodeJS.Timeout;
resolve: (value: any) => void;
reject: (error: any) => void;
}[]>();
private delay: number;
constructor(delayMs: number = 300) {
this.delay = delayMs;
}
async fetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
// Get or create pending array
if (!this.pending.has(key)) {
this.pending.set(key, []);
}
const pending = this.pending.get(key)!;
// Clear existing timer
if (pending.length > 0) {
clearTimeout(pending[0].timer);
}
// Create new timer
const timer = setTimeout(async () => {
try {
// Execute fetcher once
const result = await fetcher();
// Resolve all pending promises
pending.forEach(p => p.resolve(result));
// Clear pending
this.pending.delete(key);
} catch (error) {
// Reject all pending promises
pending.forEach(p => p.reject(error));
this.pending.delete(key);
}
}, this.delay);
// Add to pending
pending.push({ timer, resolve, reject });
});
}
}
const deduplicator = new DebouncedDeduplicator(300);
// Usage - rapid calls are debounced
async function search(query: string): Promise<Results> {
return deduplicator.fetch(`search:${query}`, async () => {
const response = await fetch(`/api/search?q=${query}`);
return response.json();
});
}
// User types "hello" quickly
search('h'); // ⏱️ timer starts
search('he'); // ⏱️ timer resets
search('hel'); // ⏱️ timer resets
search('hell'); // ⏱️ timer resets
search('hello'); // ⏱️ timer resets
// After 300ms: ONE request for "hello"📊 Comparison Table
| Pattern | Complexity | Use Case | Library Needed |
|---|---|---|---|
| Request Pool | Low | Simple deduplication | None |
| React Query | Low | React apps | @tanstack/react-query |
| SWR | Low | React apps | swr |
| Custom Hook | Medium | Custom logic | None |
| Time-Based | Medium | Time window | None |
| DataLoader | Medium | GraphQL batching | dataloader |
| Debounced | High | Search, autocomplete | None |
🏢 Real-World Examples
// DataLoader for GraphQL
// Batches friend requests
// Deduplicates profile fetches// Request pooling
// Dedupe timeline requests
// Batch avatar loadsAmazon
// Aggressive deduplication
// Product data batched
// Image requests pooled📚 Key Takeaways
- Use React Query or SWR - Handles automatically
- Request pooling for custom solutions
- Time-based for repeated calls
- DataLoader for GraphQL batching
- Debounce for user input (search)
- Monitor duplicate requests in DevTools
- Test with multiple components
Deduplication is free performance. Always enable it.