SWR Reference: useSWR, Mutations, Infinite Scroll, Revalidation & Next.js
SWR (stale-while-revalidate) is Vercel’s lightweight data-fetching hook for React. The strategy: return cached data immediately (stale), then fetch in the background (revalidate). It’s lighter than TanStack Query and integrates seamlessly with Next.js. The key difference from TanStack Query: SWR uses string keys tied to fetchers via a global config, while TanStack Query uses queryKey arrays with inline fetchers. SWR is simpler; TanStack Query has more features (mutations with optimistic updates, devtools, paginated queries).
1. Basic Usage & Global Config
useSWR, SWRConfig global fetcher, key types, and status flags
// npm install swr
import useSWR from "swr";
import { SWRConfig } from "swr";
// Global fetcher — avoids repeating fetch logic in every hook:
const fetcher = (url: string) =>
fetch(url).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
// Wrap app with SWRConfig:
function App() {
return (
<SWRConfig value={{ fetcher, revalidateOnFocus: true, dedupingInterval: 2000 }}>
<Router />
</SWRConfig>
);
}
// useSWR — key + optional fetcher override:
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading, isValidating } = useSWR<User>(
`/api/users/${userId}`, // key — also the URL when using default fetcher
// fetcher arg optional when SWRConfig provides one
);
if (isLoading) return <Spinner />;
if (error) return <p>Error: {error.message}</p>;
// isValidating = true during background revalidation (data still shown)
return (
<div>{data?.name} {isValidating && <LoadingDot />}</div>
);
}
// Conditional fetching — pass null to disable:
const { data } = useSWR(userId ? `/api/users/${userId}` : null);
// Key as array (auto-serialized):
const { data } = useSWR(["/api/users", { role: "admin", page: 1 }], fetcher);
2. Mutations & Optimistic Updates
useSWRMutation for writes, mutate() for cache updates, optimistic UI
import useSWR, { useSWRMutation } from "swr";
import { mutate } from "swr";
// useSWRMutation — for POST/PUT/DELETE:
function DeleteButton({ userId }: { userId: string }) {
const { trigger, isMutating } = useSWRMutation(
`/api/users/${userId}`,
async (url) => {
const res = await fetch(url, { method: "DELETE" });
if (!res.ok) throw new Error("Delete failed");
}
);
return (
<button onClick={() => trigger()} disabled={isMutating}>
{isMutating ? "Deleting..." : "Delete"}
</button>
);
}
// Bound mutate — update cache after mutation:
function UpdateName({ userId }: { userId: string }) {
const { data, mutate } = useSWR<User>(`/api/users/${userId}`);
async function handleUpdate(name: string) {
// Optimistic update — update cache immediately:
await mutate(
async (current) => {
const res = await fetch(`/api/users/${userId}`, {
method: "PUT",
body: JSON.stringify({ name }),
});
return res.json(); // returns new data to update cache
},
{ optimisticData: { ...data!, name }, // instant UI update
rollbackOnError: true } // revert if fetch fails
);
}
}
// Global mutate — revalidate from anywhere (no hook needed):
import { mutate } from "swr";
mutate("/api/users"); // revalidate (refetch)
mutate("/api/users", newData); // update cache + revalidate
mutate("/api/users", newData, false); // update cache, skip revalidate
3. Pagination & Infinite Scroll
useSWRInfinite for load-more and infinite scroll patterns
import useSWRInfinite from "swr/infinite";
interface Page {
users: User[];
nextCursor: string | null;
}
function UserList() {
const getKey = (pageIndex: number, previousPage: Page | null) => {
if (previousPage && !previousPage.nextCursor) return null; // reached end
if (pageIndex === 0) return "/api/users?limit=20";
return `/api/users?limit=20&cursor=${previousPage!.nextCursor}`;
};
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite<Page>(
getKey,
fetcher
);
const users = data?.flatMap((page) => page.users) ?? [];
const isLoadingMore = isValidating && size > (data?.length ?? 0);
const hasMore = data?.[data.length - 1]?.nextCursor != null;
return (
<div>
{users.map((u) => <UserCard key={u.id} user={u} />)}
{isLoading && <Spinner />}
{hasMore && (
<button onClick={() => setSize(size + 1)} disabled={isLoadingMore}>
{isLoadingMore ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}
// Infinite scroll (trigger on intersection):
// const ref = useRef(); useIntersectionObserver(ref, () => setSize(s => s + 1));
4. Revalidation, Polling & Prefetching
Auto-revalidation options, polling interval, preload(), and focus/reconnect events
// Revalidation options (per-hook or global in SWRConfig):
const { data } = useSWR("/api/metrics", fetcher, {
refreshInterval: 5000, // poll every 5 seconds
refreshWhenHidden: false, // don't poll when tab is hidden
refreshWhenOffline: false, // don't poll when offline
revalidateOnFocus: true, // refetch when window regains focus
revalidateOnReconnect: true, // refetch on network reconnect
revalidateIfStale: true, // refetch even if cached data exists
dedupingInterval: 2000, // deduplicate requests within 2s window
});
// Disable auto-revalidation for static data:
const { data } = useSWR("/api/config", fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
// data only fetches once per mount
});
// Preload — warm cache before component renders:
import { preload } from "swr";
preload("/api/users", fetcher); // call on hover or in parent component
// Next.js — preload in Server Component, use in Client Component:
// In page.tsx (server): preload("/api/users", fetcher);
// In UserList.tsx (client): const { data } = useSWR("/api/users"); // instant
// Error retry with exponential backoff (default):
const { data } = useSWR("/api/data", fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (error.status === 404) return; // don't retry 404s
if (retryCount >= 3) return; // max 3 retries
setTimeout(() => revalidate({ retryCount }), 5000 * (retryCount + 1));
},
});
5. TypeScript, Next.js App Router & Suspense
Typed fetchers, Next.js integration patterns, and Suspense mode
// Typed useSWR:
interface User { id: string; name: string; email: string; }
const { data } = useSWR<User, Error>("/api/me", fetcher);
// data: User | undefined error: Error | undefined
// Custom hook pattern (recommended):
function useUser(id: string) {
const { data, error, isLoading, mutate } = useSWR<User>(
id ? `/api/users/${id}` : null,
fetcher,
{ revalidateOnFocus: false }
);
return { user: data, error, isLoading, mutateUser: mutate };
}
// Suspense mode — let React Suspense handle loading:
const { data } = useSWR("/api/users", fetcher, { suspense: true });
// No isLoading check — component suspends until data is ready
// Wrap with: <Suspense fallback={<Spinner />}><Users /></Suspense>
// Next.js App Router — server component preload:
import { unstable_serialize } from "swr";
export default async function Page() {
const users = await fetchUsers(); // server-side fetch
return (
<SWRConfig value={{ fallback: { "/api/users": users } }}>
<UsersClient />
</SWRConfig>
);
}
// UsersClient uses useSWR("/api/users") — data from fallback on first render
// SWR vs TanStack Query:
// Use SWR when: Next.js project, simple GET-heavy API, smaller bundle size needed
// Use TanStack Query when: complex mutations with optimistic updates, devtools needed, fine-grained cache invalidation
Track Node.js and React ecosystem releases at ReleaseRun. Related: TanStack Query v5 Reference | Next.js App Router Reference | React Reference | React EOL Tracker
🔍 Free tool: npm Package Health Checker — check SWR and related data-fetching packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com