Cache Strategies
Cache Strategies
Different approaches to caching based on use case.
Cache-Aside (Lazy Loading)
// Most common pattern - check cache first, fetch if missing
function useProduct(id) {
return useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes (garbage collection)
});
}
// Usage
function ProductPage({ id }) {
const { data, isLoading, isStale } = useProduct(id);
return (
<div>
{isLoading ? <Spinner /> : (
<>
<h1>{data.name}</h1>
{isStale && <span className="stale-indicator">Refreshing...</span>}
</>
)}
</div>
);
}
Read-Through Cache
// Cache always returns data, fetching in background
function useProductsWithReadThrough() {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000,
refetchOnMount: 'always', // Always refetch on mount
refetchOnWindowFocus: true, // Refetch when window gains focus
});
}
Write-Through Cache
// Update cache immediately on write
const addProduct = useMutation({
mutationFn: createProduct,
onSuccess: (newProduct) => {
queryClient.setQueryData(['products'], (old) => [...old, newProduct]);
},
});
Write-Behind (Write-Back)
// Queue writes and batch them
function useWriteBehindCache() {
const pendingWrites = useRef([]);
const queueWrite = useCallback((operation) => {
pendingWrites.current.push(operation);
// Debounce batch write
if (pendingWrites.current.length === 1) {
setTimeout(() => flushWrites(), 1000);
}
}, []);
const flushWrites = async () => {
const writes = pendingWrites.current.splice(0);
if (writes.length === 0) return;
try {
await batchAPI(writes);
} catch (error) {
// Re-queue failed writes
pendingWrites.current.unshift(...writes);
}
};
return { queueWrite };
}
Choosing a Strategy
| Strategy | Use Case |
|---|---|
| Cache-Aside | General purpose, read-heavy |
| Read-Through | Frequently accessed data |
| Write-Through | Data consistency critical |
| Write-Behind | High write throughput |
Stale-While-Revalidate
Stale-While-Revalidate
Show cached data immediately while fetching fresh data in the background.
React Query Implementation
// Default stale-while-revalidate behavior
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 60 * 1000, // Data is fresh for 1 minute
gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
});
}
// Configure per query
function useProduct(id) {
return useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
staleTime: id === 'featured' ? 0 : 60000, // Featured products always fresh
gcTime: Infinity, // Never remove from cache
});
}
Visual Indicators
function ProductList() {
const { data, isLoading, isFetching, isStale } = useProducts();
return (
<div>
{isLoading ? (
<Spinner />
) : (
<>
{isStale && isFetching && (
<div className="refresh-banner">
Updating data...
</div>
)}
<ProductGrid products={data} />
</>
)}
</div>
);
}
Custom Stale-While-Revalidate Hook
function useSWRHook(key, fetcher, options = {}) {
const { dedupingInterval = 2000 } = options;
const [data, setData] = useState(() => cache.get(key));
const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
let cancelled = false;
const lastUpdated = cache.getTimestamp(key);
const isStale = !lastUpdated ||
Date.now() - lastUpdated > (options.staleTime || 0);
if (isStale) {
setIsValidating(true);
fetcher(key)
.then((result) => {
if (!cancelled) {
setData(result);
cache.set(key, result);
}
})
.finally(() => {
if (!cancelled) setIsValidating(false);
});
}
return () => { cancelled = true; };
}, [key, fetcher, options.staleTime]);
return { data, isValidating, isStale: !cache.getTimestamp(key) };
}
Benefits
- Instant UI: Show cached data immediately
- Background updates: Fetch fresh data without blocking
- Bandwidth efficient: Only fetch when stale
- Better UX: No loading spinners for cached data
Cache Invalidation
Cache Invalidation
Keep cache consistent with server state.
Invalidation Strategies
// Exact key invalidation
queryClient.invalidateQueries({ queryKey: ['product', 123] });
// Partial key match
queryClient.invalidateQueries({ queryKey: ['products'] });
// All queries
queryClient.invalidateQueries();
// Predicate-based
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'products',
});
Mutation-Based Invalidation
// After any mutation, invalidate related queries
const updateProduct = useMutation({
mutationFn: (product) =>
fetch(`/api/products/${product.id}`, {
method: 'PUT',
body: JSON.stringify(product),
}),
onSuccess: (data, product) => {
// Invalidate specific product
queryClient.invalidateQueries({ queryKey: ['product', product.id] });
// Invalidate product list
queryClient.invalidateQueries({ queryKey: ['products'] });
// Invalidate any query starting with 'products'
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
Optimistic Invalidation
const deleteProduct = useMutation({
mutationFn: (id) => fetch(`/api/products/${id}`, { method: 'DELETE' }),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previous = queryClient.getQueryData(['products']);
// Optimistically remove
queryClient.setQueryData(['products'], (old) =>
old.filter((p) => p.id !== id)
);
return { previous };
},
onError: (err, id, context) => {
queryClient.setQueryData(['products'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
Cache Time Configuration
// Global defaults
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
},
},
});
// Per-query overrides
useQuery({
queryKey: ['user'],
queryFn: fetchUser,
staleTime: Infinity, // Never go stale (user data rarely changes)
gcTime: Infinity, // Keep in cache forever
});
Common Pitfalls
- Over-invalidation: Invalidating too many queries causes unnecessary refetches
- Under-invalidation: Missing invalidation causes stale data
- Forgetting to invalidate: Mutations without invalidation leave cache stale
- Not handling errors: Failed mutations should not invalidate cache
Practice Problems
Create a reusable React component implementing Client-Side Caching. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Client-Side Caching using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Client-Side Caching for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is the cache-aside pattern?
2. What does staleTime control?
3. When should you invalidate cache?
4. What is the benefit of stale-while-revalidate?
5. What is garbage collection in caching?
Flashcards
Question
What is cache-aside pattern?
Click to reveal answer
Answer
Check cache first; if missing or stale, fetch from server and update cache.
Question
What is stale-while-revalidate?
Click to reveal answer
Answer
Show cached (possibly stale) data immediately, then fetch fresh data in the background.
Question
When should you invalidate cache?
Click to reveal answer
Answer
After mutations that change server state, to keep the UI consistent with the server.
Question
What is the difference between staleTime and gcTime?
Click to reveal answer
Answer
staleTime is when data needs refetching; gcTime is when data is removed from cache entirely.
Question
What is Client-Side Caching?
Click to reveal answer
Answer
Client-Side Caching is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Choose cache strategy based on read/write patterns
- 2.stale-while-revalidate provides instant UI with background updates
- 3.Always invalidate cache after mutations that change server state
- 4.Configure staleTime and gcTime based on data freshness needs
- 5.Use visual indicators for data freshness status
Interview Tips
- •Explain the cache-aside pattern and when to use it
- •Describe stale-while-revalidate and its benefits
- •Discuss cache invalidation strategies and pitfalls
Cheat Sheet
Client-Side Caching Cheat Sheet
React Query Cache Config
useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: 60000, // 1 min fresh
gcTime: 300000, // 5 min in cache
});
Invalidation
// Invalidate specific
queryClient.invalidateQueries({ queryKey: ['product', id] });
// Invalidate all with prefix
queryClient.invalidateQueries({ queryKey: ['products'] });
Cache Strategies
- Cache-Aside: Check cache, fetch if needed
- Read-Through: Cache fetches automatically
- Write-Through: Write to cache and server
- Write-Behind: Queue writes for batch