SWR Caching Strategy and Revalidation
SWR's core strategy is stale-while-revalidate: serve cached data immediately while quietly refreshing it in the background. This pattern keeps your UI responsive—users see instant results—while ensuring data freshness without blocking interaction. SWR manages a single in-memory cache keyed by URL; multiple requests to the same URL within milliseconds deduplicate into one HTTP request. Understanding revalidation triggers (focus, reconnect, interval, manual) and tuning cache behavior is essential for performance and user experience.
How SWR's Cache Works
When you call useSWR('/api/users', fetcher) for the first time, SWR:
- Checks if the URL is cached (and not expired)
- If not cached, immediately fires the fetch and returns
isLoading: true - On fetch success, stores the response in memory and notifies all components using that URL
- On subsequent calls to the same URL, returns cached data instantly (
isLoading: false)
The cache is global—shared across all components. If two components fetch /api/users within 2 seconds (the default dedupingInterval), only one HTTP request fires. The second component hooks into the first's fetch.
// Both components fetch the same URL
const Component1 = () => useSWR('/api/users', fetcher);
const Component2 = () => useSWR('/api/users', fetcher);
// Result: one HTTP request, both components receive the response
This deduplication is automatic and transparent. No Redux, no context providers—just intelligent hook-level caching.
Stale Data and Revalidation Triggers
SWR considers cached data stale after a configurable period. By default, the following events trigger automatic revalidation:
Window focus: When the user switches back to your tab, SWR revalidates. Useful for data that changes frequently elsewhere:
const { data, isValidating } = useSWR('/api/chat-messages', fetcher, {
revalidateOnFocus: true, // Default
focusThrottleInterval: 5000, // Max once per 5 seconds
});
// User tabs away, scrolls social media, tabs back → auto-revalidates
Network reconnect: When the connection goes offline and comes back online:
const { data } = useSWR('/api/balance', fetcher, {
revalidateOnReconnect: true, // Default
});
// Internet drops, comes back → auto-revalidates
Interval: Periodically refresh in the background:
const { data } = useSWR('/api/stock-price', fetcher, {
refreshInterval: 3000, // Revalidate every 3 seconds
});
// Continuously refreshes while component is mounted
Manual: Call mutate() to revalidate immediately:
const { mutate } = useSWR('/api/posts', fetcher);
async function handlePostCreated(newPost) {
await api.createPost(newPost);
mutate(); // Refresh immediately
}
Deduplication and the dedupingInterval
The dedupingInterval (default 2000 ms) prevents duplicate requests. Requests to the same URL within this window merge:
// Scenario: you render multiple components at once
<UserProfile userId={1} />
<UserStats userId={1} />
<RecentActivity userId={1} />
// All three call useSWR('/api/users/1', fetcher) within milliseconds
// Result: one HTTP request fires, all three hooks receive the response
Lower dedupingInterval if you need fresher inter-component syncing:
useSWR('/api/live-count', fetcher, {
dedupingInterval: 0, // Every request is fresh (no dedup)
});
Optimizing Cache Behavior
Disable aggressive revalidation for static data:
const { data } = useSWR('/api/blog-post/123', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
// Only revalidate if explicitly called
});
Enable aggressive revalidation for real-time data:
const { data } = useSWR('/api/current-user-activity', fetcher, {
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 2000, // Every 2 seconds
focusThrottleInterval: 1000, // Throttle focus revalidations
});
Use revalidateIfStale to avoid unnecessary refetches if data is fresh:
const { data } = useSWR('/api/user', fetcher, {
revalidateIfStale: true, // Default: always revalidate even if fresh
});
Set to false to skip revalidation if data is already recent.
Manual Mutations and Optimistic Updates
After a mutation, call mutate() to revalidate:
export function CreateComment({ postId }) {
const { mutate } = useSWR(`/api/posts/${postId}/comments`, fetcher);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// Optimistic update: assume success immediately
const optimisticComment = {
id: Math.random(),
text: formData.get('text'),
createdAt: new Date(),
};
mutate(
(oldComments) => [optimisticComment, ...oldComments],
false // false = don't revalidate yet
);
try {
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
body: formData,
});
const newComment = await response.json();
// Revalidate to sync server truth
mutate();
} catch (error) {
// Rollback on error
mutate();
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
Key Configuration Options
| Option | Default | Purpose |
|---|---|---|
revalidateOnFocus | true | Refresh when window regains focus |
revalidateOnReconnect | true | Refresh when network reconnects |
revalidateIfStale | true | Always revalidate if data exists |
refreshInterval | 0 (disabled) | Periodically refresh in milliseconds |
refreshWhenHidden | false | Refresh even when tab is hidden |
dedupingInterval | 2000 | Merge requests within this window |
focusThrottleInterval | 5000 | Max frequency of focus revalidations |
errorRetryCount | 5 | Retry failed requests this many times |
errorRetryInterval | 5000 | Wait before retrying after error |
Comparing Cache Strategies
SWR's implicit caching:
- Pros: zero setup, automatic deduplication, clean component code
- Cons: no fine-grained control, cache limited to memory, no offline support
RTK Query's explicit caching (tags and subscribers):
- Pros: precise invalidation, offline-persisted cache, time-travel debugging
- Cons: more boilerplate, Redux required
For most apps, SWR's simplicity wins. For complex multi-endpoint orchestration, RTK Query's explicitness prevents bugs.
Key Takeaways
- SWR caches responses by URL in memory; multiple components fetch the same URL only once per
dedupingInterval - Revalidation triggers include window focus, reconnect, intervals, and manual
mutate()calls - Tune
revalidateOnFocus,refreshInterval, andfocusThrottleIntervalto match your data freshness requirements - Optimistic updates via
mutate(optimisticData, false)make mutations feel instant - SWR's simplicity trades for less control; RTK Query offers more granular invalidation
Frequently Asked Questions
How do I clear the SWR cache?
SWR has no built-in clear-all API. Cache lives in component memory and clears on unmount. For logout scenarios, set the fetcher to return a promise that rejects, or use conditional keys:
const key = isLoggedIn ? '/api/user' : null;
const { data } = useSWR(key, fetcher);
Can I persist SWR cache to localStorage?
Not built-in. You'd manually save data to localStorage after each fetch and initialize with it on reload. TanStack Query has persistence plugins.
What if my API returns different data for the same URL based on headers?
SWR's key is URL-only; headers aren't part of the key. Include header-dependent info in the URL or use a custom key function:
const key = [`/api/posts`, { token: currentToken }];
const { data } = useSWR(key, (k) => fetcher(k[0], { headers: { token: k[1].token } }));
Does mutate() accept a promise?
Yes. Pass a promise and mutate() will await it and revalidate after:
mutate(fetchNewData()).then(() => console.log('Done'));
How do I debug SWR cache misses?
Enable SWR's logger in development:
import useSWR, { SWRConfig } from 'swr';
<SWRConfig value={{ onError: (error, key) => console.error(key, error) }}>
<App />
</SWRConfig>