Data Fetching Strategies
Master network request patterns for optimal performance and user experience
Choosing the right data fetching strategy can make or break your app's performance and user experience. Whether you're building a real-time dashboard or a static blog, each approach has its own trade-offs. Let's break down the three primary strategies—Network Only, Network + Cache, and Cache Only—in a practical and friendly way.
Network Only
Always fetch data straight from the server.
Pros:
- Always Fresh: You get the most up-to-date info, straight from the source
- Great for Real-Time: Ideal for dashboards, live stats, or anything time-sensitive
- Simple Implementation: No cache management complexity
Cons:
- Slower Load: Network latency might slow things down
- No Offline Support: If the network's down, your app's data is too
- Higher Server Load: Every request hits your backend
Implementation Example
// Simple network-only fetch
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Network error:', error);
throw error;
}
}
// React component example
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadUser() {
try {
setLoading(true);
const userData = await fetchUserData(userId);
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Network + Cache (Stale-While-Revalidate)
Use both the cache and the network together, often in a "stale-while-revalidate" style.
Pros:
- Best of Both Worlds: Instant response from cache + background update from network
- Smooth UX: Feels fast and fresh when done right
- Offline Resilience: Can work with cached data when offline
Cons:
- Can Be Tricky: Managing when to show cached data vs. fresh data can get complex
- Stale Risk: Users may briefly see outdated content
- More Complex Implementation: Requires cache management logic
Implementation Example
// Cache with network fallback
class DataFetcher {
constructor() {
this.cache = new Map();
}
async fetchWithCache(url, options = {}) {
const cacheKey = `${url}-${JSON.stringify(options)}`;
const cached = this.cache.get(cacheKey);
// Return cached data immediately if available
if (cached && Date.now() - cached.timestamp < (options.maxAge || 5 * 60 * 1000)) {
return cached.data;
}
try {
// Fetch fresh data
const response = await fetch(url, options);
if (!response.ok) throw new Error('Network error');
const data = await response.json();
// Update cache
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
// Fallback to stale cache if available
if (cached) {
console.warn('Using stale cache due to network error');
return cached.data;
}
throw error;
}
}
}
// React Query example (recommended approach)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserData(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Cache Only
Pull data only from local storage or in-memory cache.
Pros:
- Super Fast: Lightning-quick responses without hitting the network
- Works Offline: Perfect for PWAs and apps with predictable data
- Reduces Server Load: No unnecessary network requests
Cons:
- Staleness Danger: Without sync, you risk showing old or inaccurate info
- No Fallback: If it's not in the cache, there's no data
- Storage Limitations: Limited by browser storage capacity
Implementation Example
// Cache-only implementation
class CacheOnlyFetcher {
constructor() {
this.cache = new Map();
}
async get(key) {
const cached = this.cache.get(key);
if (!cached) {
throw new Error('Data not found in cache');
}
return cached.data;
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
// Pre-populate cache (usually done at app startup)
async preloadData() {
const staticData = await fetch('/api/static-data');
const data = await staticData.json();
// Cache all static data
Object.entries(data).forEach(([key, value]) => {
this.set(key, value);
});
}
}
// React component example
function StaticContent({ contentKey }) {
const [content, setContent] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
try {
const cachedContent = cacheOnlyFetcher.get(contentKey);
setContent(cachedContent);
} catch (err) {
setError('Content not available offline');
}
}, [contentKey]);
if (error) return <div>{error}</div>;
if (!content) return <div>Loading...</div>;
return <div>{content}</div>;
}Choosing the Right Strategy
The strategy you choose should align with your app's core needs:
Use Network Only for:
- Real-time features like notifications or live feeds
- Critical data that must be fresh (banking, stock prices)
- Simple apps where cache complexity isn't worth it
Opt for Network + Cache for:
- Blogs, user profiles, or data that updates periodically
- E-commerce product listings
- Social media feeds
- Most web applications
Go with Cache Only for:
- Static content, offline apps, or embedded devices
- Configuration data that rarely changes
- PWA offline functionality
Advanced Patterns
Service Worker Caching
// Service worker for advanced caching
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version if available
if (response) {
return response;
}
// Fetch from network
return fetch(event.request)
.then((response) => {
// Cache the response for future use
const responseClone = response.clone();
caches.open('api-cache').then((cache) => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
}
});SWR Pattern (Stale-While-Revalidate)
import useSWR from 'swr';
function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: true,
refreshInterval: 30000, // Refresh every 30 seconds
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading user</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}Performance Comparison
| Strategy | Initial Load | Subsequent Loads | Offline Support | Data Freshness |
|---|---|---|---|---|
| Network Only | Slow | Slow | ❌ | ✅ Always Fresh |
| Network + Cache | Fast | Very Fast | ✅ | ⚠️ Potentially Stale |
| Cache Only | Very Fast | Very Fast | ✅ | ❌ May Be Stale |
Final Thoughts
Track performance with tools like Chrome DevTools, Lighthouse, or React Profiler, and adapt as your app scales. The best approach often combines multiple strategies based on different types of data in your application.
Want to go deeper? Pair this with service workers, SWR/React Query, and IndexedDB strategies to unlock serious frontend power 💪.
Next: Apollo Client Caching - Learn how to master Apollo Client's powerful caching system.