Skip to main content

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:

  1. Checks if the URL is cached (and not expired)
  2. If not cached, immediately fires the fetch and returns isLoading: true
  3. On fetch success, stores the response in memory and notifies all components using that URL
  4. 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

OptionDefaultPurpose
revalidateOnFocustrueRefresh when window regains focus
revalidateOnReconnecttrueRefresh when network reconnects
revalidateIfStaletrueAlways revalidate if data exists
refreshInterval0 (disabled)Periodically refresh in milliseconds
refreshWhenHiddenfalseRefresh even when tab is hidden
dedupingInterval2000Merge requests within this window
focusThrottleInterval5000Max frequency of focus revalidations
errorRetryCount5Retry failed requests this many times
errorRetryInterval5000Wait 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, and focusThrottleInterval to 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>

Further Reading