Caching Strategies
Recipe
SWR uses an in-memory cache by default. Customize the cache provider for persistent storage, design your key strategy for maximum cache hits, and leverage deduplication to avoid redundant network requests.
"use client";
import { SWRConfig } from "swr";
function App({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
provider: () => new Map(), // Default in-memory cache
dedupingInterval: 2000, // Dedup requests within 2s
}}
>
{children}
</SWRConfig>
);
}Working Example
"use client";
import { SWRConfig, Cache } from "swr";
import useSWR from "swr";
// Persistent cache backed by localStorage
function localStorageProvider(): Cache {
const map = new Map<string, any>(
JSON.parse(localStorage.getItem("swr-cache") || "[]")
);
// Persist to localStorage before page unload
window.addEventListener("beforeunload", () => {
const appCache = JSON.stringify(Array.from(map.entries()));
localStorage.setItem("swr-cache", appCache);
});
return {
get: (key: string) => map.get(key),
set: (key: string, value: any) => map.set(key, value),
delete: (key: string) => map.delete(key),
keys: () => map.keys(),
};
}
export function CachedApp({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={{ provider: localStorageProvider }}>
{children}
</SWRConfig>
);
}
// Components automatically benefit from persistent cache
function UserProfile() {
const { data } = useSWR("/api/me", (url) => fetch(url).then((r) => r.json()));
return <div>{data?.name}</div>;
}Deep Dive
How It Works
- SWR's cache is a simple key-value store. The key from
useSWR(key)maps directly to a cache entry. - The default cache is a
Mapinstance that lives in memory and resets on page reload. - Deduplication: When multiple components request the same key within
dedupingIntervalms, only one fetch runs. All hooks receive the same promise. - Cache entries store data, error, and metadata (timestamps). SWR uses these to decide if data is stale.
- The
providerfunction inSWRConfigis called once on mount and must return aCache-compatible object.
Variations
Key design for cache efficiency:
// Good: Stable, serializable keys
useSWR(`/api/users/${id}`, fetcher);
useSWR(["/api/users", id, "posts"], fetcher);
// Bad: Unstable object references (not serializable)
useSWR({ url: "/api/users", id }, fetcher); // Will not cache properlyPrefetching into cache:
import { preload } from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
// Prefetch on hover or route prefetch
function ProductCard({ id }: { id: string }) {
const handleMouseEnter = () => {
preload(`/api/products/${id}`, fetcher);
};
return (
<a href={`/products/${id}`} onMouseEnter={handleMouseEnter}>
View Product
</a>
);
}Clearing the cache:
import { useSWRConfig } from "swr";
function LogoutButton() {
const { cache } = useSWRConfig();
const handleLogout = () => {
// Clear all cached data on logout
if (cache instanceof Map) {
cache.clear();
}
};
return <button onClick={handleLogout}>Logout</button>;
}Cache with TTL:
function ttlProvider(): Cache {
const cache = new Map<string, { value: any; expiry: number }>();
const TTL = 5 * 60 * 1000; // 5 minutes
return {
get: (key: string) => {
const entry = cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
cache.delete(key);
return undefined;
}
return entry.value;
},
set: (key: string, value: any) => {
cache.set(key, { value, expiry: Date.now() + TTL });
},
delete: (key: string) => cache.delete(key),
keys: () => cache.keys(),
};
}TypeScript Notes
- Import
Cachefromswrfor typing custom providers. - The
provideroption expects() => Cache.
import type { Cache } from "swr";
const provider = (): Cache => {
const map = new Map();
return {
get: (key: string) => map.get(key),
set: (key: string, value: any) => map.set(key, value),
delete: (key: string) => map.delete(key),
keys: () => map.keys(),
};
};Gotchas
- The default in-memory cache is lost on page reload. If you need persistence, implement a custom provider.
dedupingIntervalonly deduplicates concurrent requests. It does not prevent re-fetching after the interval expires.- Array keys like
["/api/users", id]are serialized for caching. The order of elements matters:[a, b]and[b, a]are different cache keys. - Custom cache providers must implement the full
Cacheinterface includingkeys(). Missing methods will cause runtime errors. - localStorage has a ~5MB limit. Large caches can silently fail. Implement error handling in your provider's
setmethod. - Do not share cache state between
SWRConfigproviders at different nesting levels; each provider creates its own scope.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Default Map cache | Zero config, fast | Lost on reload |
| localStorage provider | Persists across sessions | 5MB limit, sync API blocks main thread |
| IndexedDB provider | Large storage, async | More complex implementation |
| No cache (dedupingInterval: 0) | Always fresh data | More network requests |
FAQs
What is the default cache implementation in SWR?
SWR uses an in-memory Map instance by default. It resets on page reload. No configuration is needed for basic caching.
How do I make the SWR cache persist across page reloads?
Implement a custom cache provider backed by localStorage or IndexedDB. The provider must implement the Cache interface (get, set, delete, keys). Persist on beforeunload and rehydrate on initialization.
What is the difference between deduplication and caching in SWR?
- Deduplication (
dedupingInterval) prevents concurrent duplicate requests within a time window. - Caching stores data across the full session so subsequent renders use cached values.
- Deduplication does not prevent re-fetching after the interval expires.
Gotcha: Does the order of elements in array keys matter for caching?
Yes. ["/api/users", id] and [id, "/api/users"] are different cache keys. SWR serializes array elements in order, so changing the order creates a different cache entry.
How do I prefetch data into the SWR cache?
import { preload } from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
// Prefetch on hover
const handleMouseEnter = () => {
preload(`/api/products/${id}`, fetcher);
};How do I clear the entire SWR cache (e.g., on logout)?
const { cache } = useSWRConfig();
const handleLogout = () => {
if (cache instanceof Map) {
cache.clear();
}
};Can I use object keys for SWR caching?
No. Object references are not serializable by SWR. Use strings or arrays as keys. useSWR({ url: "/api/users", id }, fetcher) will not cache properly.
Gotcha: What happens if localStorage exceeds its ~5MB limit in a custom provider?
The write silently fails. Implement error handling in your provider's set method using a try/catch, and consider evicting old entries or falling back to in-memory storage.
How do I type a custom cache provider in TypeScript?
import type { Cache } from "swr";
const provider = (): Cache => {
const map = new Map();
return {
get: (key: string) => map.get(key),
set: (key: string, value: any) => map.set(key, value),
delete: (key: string) => map.delete(key),
keys: () => map.keys(),
};
};What TypeScript type should the provider option be in SWRConfig?
The provider option expects a function of type () => Cache. Import Cache from swr to type your custom implementation. Missing interface methods (keys, delete) will cause runtime errors.
How does a TTL-based cache provider work with SWR?
Store entries with an expiry timestamp. On get, check if the entry has expired and return undefined if so. This forces SWR to treat it as a cache miss and re-fetch.