Skip to content

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