Cache Strategies (SWR)
Patterns for efficient data fetching and caching
SWR (Stale-While-Revalidate) is a caching strategy where you serve cached (stale) data immediately while simultaneously fetching fresh data in the background. Once the new data arrives, the cache is updated and the UI refreshes. This pattern provides instant UI feedback while ensuring data eventually becomes consistent.
The Problem
Traditional data fetching patterns:
// Pattern 1: Loading states
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
return <Content data={data} />;
Problems:
- User sees loading spinner every time
- Slow perceived performance
- No offline support
The SWR Pattern
import useSWR from 'swr';
function Profile() {
// Returns cached data immediately, revalidates in background
const { data, error, isLoading } = useSWR('/api/user', fetcher);
// Show stale data while loading, or show loading if no cache
if (isLoading && !data) return <Spinner />;
if (error) return <Error />;
return (
<div>
<h1>{data.name}</h1>
{isLoading && <span>Refreshing...</span>}
</div>
);
}
User experience:
- First visit: Shows spinner → shows data
- Return visit: Shows cached data immediately → updates in background
- Offline: Shows cached data (if available)
How It Works
1. Cache Lookup
User requests data
↓
Check cache
↓
┌──────────────────────────────────────┐
│ Cache hit? │
│ YES → Return immediately (stale) │
│ NO → Wait for fetch │
└──────────────────────────────────────┘
↓
Revalidate in background
↓
Update cache + UI
2. Deduplication
// Multiple components request same data
function ComponentA() {
const { data } = useSWR('/api/user', fetcher);
return ...;
}
function ComponentB() {
const { data } = useSWR('/api/user', fetcher); // Same key!
return ...;
}
// Only ONE request is made, shared between both components
3. Revalidation Triggers
SWR automatically revalidates when:
- Component mounts
- Window regains focus
- Network reconnects
- Interval elapsed
const { data } = useSWR('/api/data', fetcher, {
revalidateOnFocus: true, // Revalidate when window focuses
revalidateOnReconnect: true, // Revalidate when back online
refreshInterval: 5000, // Poll every 5 seconds
});
HTTP Cache Integration
Cache-Control Headers
// Server sets caching headers
app.get('/api/data', (req, res) => {
res.set('Cache-Control', 'max-age=60, stale-while-revalidate=300');
res.json(data);
});
| Header | Meaning |
|---|---|
max-age=60 | Fresh for 60 seconds |
stale-while-revalidate=300 | Can serve stale for 5 min while revalidating |
SWR + HTTP Cache Strategy
const fetcher = async (url) => {
const response = await fetch(url, {
// Use fetch with HTTP caching
cache: 'default', // Respect Cache-Control headers
});
return response.json();
};
Advanced Patterns
1. Optimistic UI
Update UI before API confirms:
const { mutate } = useSWR('/api/todos');
async function addTodo(newTodo) {
// Optimistically update
mutate(
current => [...current, newTodo],
false // Don't revalidate yet
);
try {
await api.addTodo(newTodo);
// Revalidate to confirm
mutate();
} catch (error) {
// Rollback on error
mutate();
}
}
2. Pagination with Cache
function TodoList() {
const [page, setPage] = useState(1);
const { data, error } = useSWR(
`/api/todos?page=${page}`,
fetcher,
{ keepPreviousData: true } // Show old data while loading new page
);
return (
<div>
{data} {/* keepPreviousData keeps old data in `data` until new data loads */}
<button onClick={() => setPage(p => p + 1)}>Next</button>
</div>
);
}
3. Conditional Fetching
// Only fetch when userId is available
const { data } = useSWR(
userId ? `/api/user/${userId}` : null,
fetcher
);
4. Global Configuration
// pages/_app.js
import { SWRConfig } from 'swr';
function MyApp({ Component, pageProps }) {
return (
<SWRConfig
value={{
fetcher: (url) => fetch(url).then(r => r.json()),
revalidateOnFocus: false,
refreshInterval: 60000,
}}
>
<Component {...pageProps} />
</SWRConfig>
);
}
Implementing Without SWR
Custom implementation:
const cache = new Map();
function useCustomSWR(key, fetcher) {
const [data, setData] = useState(cache.get(key));
const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
let cancelled = false;
async function revalidate() {
setIsValidating(true);
try {
const fresh = await fetcher(key);
if (!cancelled) {
cache.set(key, fresh);
setData(fresh);
}
} finally {
if (!cancelled) setIsValidating(false);
}
}
// Return cached data immediately
if (!cache.has(key)) {
revalidate();
} else {
// Revalidate in background
revalidate();
}
return () => { cancelled = true; };
}, [key]);
return { data, isValidating };
}
Other Caching Strategies
Cache-First
Show cached data, only fetch if no cache:
const { data } = useSWR(key, fetcher, {
revalidateIfStale: false, // Don't revalidate
});
Network-First
Try network, fall back to cache:
const fetcher = async (url) => {
try {
return await fetch(url).then(r => r.json());
} catch (error) {
// Return from cache on error
return getFromCache(url);
}
};
Cache-Then-Network
Show cache immediately, always fetch fresh:
const { data } = useSWR(key, fetcher, {
revalidateOnMount: true,
});
Error Handling
const { data, error, isLoading } = useSWR('/api/data', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Stop retrying after 3 attempts
if (retryCount >= 3) return;
// Retry after 5 seconds
setTimeout(() => revalidate({ retryCount }), 5000);
},
errorRetryCount: 3,
});
SWR provides a superior user experience by showing cached data immediately while refreshing in the background. This eliminates loading states for returning users, provides offline support, and reduces server load through intelligent deduplication. Combine with proper HTTP caching headers for optimal performance.
When to Use SWR
✅ Good for:
- Dashboard data that updates periodically
- User profiles and settings
- Lists that change infrequently
- Offline-first applications
❌ Not ideal for:
- Real-time data (use WebSockets)
- Financial transactions (need strong consistency)
- Search results (always fresh)
- Data that must never be stale