Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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>;
}"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>
);
}visibilitychange and focus events.setInterval that fires every refreshInterval ms. The timer pauses when the tab is hidden unless refreshWhenHidden is true.online event).mutate() (bound) or mutate(key) (global) without arguments.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
});SWRConfiguration and fully typed.refreshInterval accepts number or a function (latestData: Data) => number for dynamic intervals.const { data } = useSWR<JobStatus>("/api/job", fetcher, {
refreshInterval: (data) => (data?.complete ? 0 : 2000),
});refreshInterval: 0 disables interval polling (it is the default). Setting a very low interval (under 1000ms) can overload your API.refreshWhenHidden: true keeps polling even when the user is on another tab. Use only when real-time updates matter even in background tabs.mutate() with no arguments revalidates by re-fetching. mutate(data) replaces cache data without re-fetching. These are very different operations.revalidateIfStale: false, SWR will not re-fetch on mount if there is cached data, even if that cached data is hours old.| 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 |
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.
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.
mutate() revalidates by re-fetching from the server.mutate(data) replaces the cached data directly without re-fetching.import { mutate } from "swr";
mutate(
(key) => typeof key === "string" && key.startsWith("/api/"),
undefined,
{ revalidate: true }
);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.
const { data } = useSWR("/api/job-status", fetcher, {
refreshInterval: data?.status === "running" ? 1000 : 0,
});Setting refreshInterval to 0 disables polling.
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.
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).
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),
});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.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥