Revalidation Strategies
Recipe
SWR provides multiple revalidation strategies: on window focus, on interval, on reconnect, and manual. Configure them globally or per-hook to keep data fresh without over-fetching.
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function StockPrice({ symbol }: { symbol: string }) {
const { data } = useSWR(`/api/stocks/${symbol}`, fetcher, {
refreshInterval: 5000, // Poll every 5 seconds
revalidateOnFocus: true, // Refresh on tab focus
revalidateOnReconnect: true, // Refresh on network recovery
refreshWhenHidden: false, // Pause when tab is hidden
refreshWhenOffline: false, // Pause when offline
});
return <span>${data?.price}</span>;
}Working Example
"use client";
import useSWR, { useSWRConfig } from "swr";
import { useState } from "react";
interface Notification {
id: string;
message: string;
read: boolean;
}
const fetcher = (url: string): Promise<Notification[]> =>
fetch(url).then((r) => r.json());
export default function NotificationCenter() {
const { data: notifications, mutate } = useSWR<Notification[]>(
"/api/notifications",
fetcher,
{
refreshInterval: 30000, // Auto-refresh every 30s
revalidateOnFocus: true, // Check on tab return
}
);
const handleManualRefresh = () => {
mutate(); // Trigger manual revalidation
};
const handleMarkAllRead = async () => {
await fetch("/api/notifications/mark-read", { method: "POST" });
mutate(); // Revalidate after mutation
};
return (
<div>
<div className="flex justify-between">
<h2>Notifications ({notifications?.filter((n) => !n.read).length ?? 0} unread)</h2>
<button onClick={handleManualRefresh}>Refresh</button>
</div>
<ul>
{notifications?.map((n) => (
<li key={n.id} className={n.read ? "opacity-50" : ""}>
{n.message}
</li>
))}
</ul>
<button onClick={handleMarkAllRead}>Mark all as read</button>
</div>
);
}Deep Dive
How It Works
- Focus revalidation fires when the user switches back to the browser tab. SWR listens to the
visibilitychangeandfocusevents. - Interval revalidation sets up a
setIntervalthat fires everyrefreshIntervalms. The timer pauses when the tab is hidden unlessrefreshWhenHiddenistrue. - Reconnect revalidation fires when the browser comes back online (the
onlineevent). - Manual revalidation is done by calling
mutate()(bound) ormutate(key)(global) without arguments. - All revalidation strategies are deduplicated. If multiple components request the same key, only one fetch runs.
- Stale data is shown immediately while the revalidation runs in the background.
Variations
Disable focus revalidation globally:
<SWRConfig value={{ revalidateOnFocus: false }}>
{children}
</SWRConfig>Conditional interval polling:
const { data } = useSWR("/api/job-status", fetcher, {
// Only poll while the job is running
refreshInterval: data?.status === "running" ? 1000 : 0,
});Manual revalidation with global mutate:
import { mutate } from "swr";
// Revalidate a specific key from anywhere
mutate("/api/users");
// Revalidate all keys matching a filter
mutate(
(key) => typeof key === "string" && key.startsWith("/api/"),
undefined,
{ revalidate: true }
);Revalidate on mount only:
const { data } = useSWR("/api/config", fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
// Only fetches if no cached data exists
});TypeScript Notes
- Revalidation options are part of
SWRConfigurationand fully typed. refreshIntervalacceptsnumberor a function(latestData: Data) => numberfor dynamic intervals.
const { data } = useSWR<JobStatus>("/api/job", fetcher, {
refreshInterval: (data) => (data?.complete ? 0 : 2000),
});Gotchas
- Focus revalidation can feel aggressive for data that rarely changes. Disable it for static-ish data like user profiles or app config.
refreshInterval: 0disables interval polling (it is the default). Setting a very low interval (under 1000ms) can overload your API.refreshWhenHidden: truekeeps polling even when the user is on another tab. Use only when real-time updates matter even in background tabs.- Manual
mutate()with no arguments revalidates by re-fetching.mutate(data)replaces cache data without re-fetching. These are very different operations. - When
revalidateIfStale: false, SWR will not re-fetch on mount if there is cached data, even if that cached data is hours old.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Focus revalidation | Automatic, no config needed | May fire too often for stable data |
| Interval polling | Simple, predictable | Wasteful if data rarely changes |
| WebSocket push | Real-time, server-driven | Requires WebSocket infrastructure |
| Server-Sent Events (SSE) | Lightweight push, HTTP-based | One-directional, limited browser connections |
| Manual revalidation | Full control | Requires explicit trigger points |
FAQs
What browser events trigger focus revalidation?
SWR listens to the visibilitychange and focus events. When the user switches back to the browser tab, SWR fires a background revalidation for all active keys.
How does interval revalidation behave when the tab is hidden?
By default, the polling timer pauses when the tab is hidden. Set refreshWhenHidden: true to continue polling in background tabs, but only do this when real-time updates matter even when the tab is not visible.
What is the difference between mutate() with no arguments and mutate(data)?
mutate()revalidates by re-fetching from the server.mutate(data)replaces the cached data directly without re-fetching.- These are very different operations with different use cases.
How do I revalidate all keys that match a pattern?
import { mutate } from "swr";
mutate(
(key) => typeof key === "string" && key.startsWith("/api/"),
undefined,
{ revalidate: true }
);What does revalidateIfStale: false do?
SWR will not re-fetch on mount if cached data already exists, even if that data is hours old. Use this for data that rarely changes (e.g., app config) combined with disabling focus and reconnect revalidation.
How can I implement conditional interval polling that stops when a job completes?
const { data } = useSWR("/api/job-status", fetcher, {
refreshInterval: data?.status === "running" ? 1000 : 0,
});Setting refreshInterval to 0 disables polling.
Gotcha: Why might focus revalidation feel too aggressive for some data?
Every time the user switches tabs and returns, SWR re-fetches. For stable data like user profiles or app config, this creates unnecessary requests. Disable it per-hook with revalidateOnFocus: false.
Gotcha: What happens with a very low refreshInterval like 100ms?
SWR will poll your API multiple times per second, which can overload your server and waste bandwidth. Keep intervals reasonable (1000ms minimum for most use cases).
How is refreshInterval typed in TypeScript?
It accepts number or a function (latestData: Data) => number for dynamic intervals:
const { data } = useSWR<JobStatus>("/api/job", fetcher, {
refreshInterval: (data) => (data?.complete ? 0 : 2000),
});Are revalidations from different components using the same key deduplicated?
Yes. All revalidation strategies are deduplicated. If multiple components use the same key, only one network request runs regardless of how many components trigger revalidation.