Skip to content
React rc data 5 min read

SWR

SWR is a tiny data-fetching library from Vercel, named after the HTTP caching directive stale-while-revalidate. The idea is simple but powerful: return cached (stale) data immediately so the UI is instant, then quietly refetch in the background and update the screen if anything changed. With a single hook you get caching, deduplication, automatic revalidation, and a great loading experience — all in roughly 4 KB.

Installing and the useSWR hook

Add SWR to a Vite + React project:

npm install swr

The core API is one hook, useSWR(key, fetcher). The key is a unique, stable identifier for the request (usually the URL). The fetcher is any async function that takes the key and returns data.

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

function Profile({ userId }) {
  const { data, error, isLoading } = useSWR(
    `https://api.github.com/users/${userId}`,
    fetcher
  );

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Failed to load profile.</p>;

  return (
    <article>
      <h2>{data.name}</h2>
      <p>{data.bio}</p>
      <span>{data.public_repos} public repos</span>
    </article>
  );
}

SWR returns a small object. The most useful fields:

FieldMeaning
dataResolved value from the fetcher (undefined until loaded)
errorThrown error from the fetcher, if any
isLoadingtrue only on the first load with no cached data
isValidatingtrue whenever a request is in flight (initial or revalidation)
mutateFunction to update or revalidate this key locally

The fetcher is not tied to fetch. Use Axios, a GraphQL client, or anything that returns a promise. SWR only cares about the key and the resolved value.

The stale-while-revalidate strategy

When two components ask for the same key, SWR deduplicates the request — only one network call fires, and both components share the cached result. The first render after a cache hit shows stale data instantly (no spinner), while isValidating flips to true as SWR confirms the data is fresh in the background. If the refetched value differs, the component re-renders with the update; if not, nothing flickers.

This is why a page navigated to a second time feels instant: the cache serves immediately and revalidation happens silently.

Revalidation triggers

SWR automatically revalidates on several events, all configurable:

const { data } = useSWR("/api/dashboard", fetcher, {
  revalidateOnFocus: true,        // refetch when the tab regains focus
  revalidateOnReconnect: true,    // refetch when the network comes back
  refreshInterval: 5000,          // poll every 5 seconds
  dedupingInterval: 2000,         // ignore duplicate calls within 2s
});
OptionDefaultEffect
revalidateOnFocustrueRefetch when the window is refocused
revalidateOnReconnecttrueRefetch after the browser reconnects
refreshInterval0Poll on an interval (ms); 0 disables polling
dedupingInterval2000Window during which identical requests are merged
keepPreviousDatafalseKeep old data visible while a new key loads

Setting the key to null (or returning null from a function) skips the request entirely — handy for dependent or conditional fetching:

function Repos({ user }) {
  // Wait until `user` exists before fetching repos.
  const { data } = useSWR(user ? `/api/users/${user.id}/repos` : null, fetcher);
  return <ul>{data?.map((r) => <li key={r.id}>{r.name}</li>)}</ul>;
}

Mutating data with mutate

mutate lets you update the cache directly — essential after a write. You can use the bound mutate returned by the hook, or the global mutate to target any key.

import useSWR, { useSWRConfig } from "swr";

function TodoList() {
  const { data: todos } = useSWR("/api/todos", fetcher);
  const { mutate } = useSWRConfig();

  async function addTodo(title) {
    const newTodo = { id: crypto.randomUUID(), title, done: false };

    // Optimistic update: show it immediately, then reconcile.
    await mutate(
      "/api/todos",
      async (current) => {
        await fetch("/api/todos", {
          method: "POST",
          body: JSON.stringify(newTodo),
        });
        return [...current, newTodo];
      },
      { optimisticData: [...(todos ?? []), newTodo], rollbackOnError: true }
    );
  }

  return (
    <>
      <button onClick={() => addTodo("Write docs")}>Add</button>
      <ul>{todos?.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
    </>
  );
}

With optimisticData the UI updates before the request resolves; rollbackOnError restores the previous value if the POST fails. Calling mutate("/api/todos") with no data argument simply triggers a revalidation of that key.

Output:

Render 1: Loading…             (no cache)
Render 2: [Write docs]         (optimistic, request in flight)
Render 3: [Write docs]         (server confirmed, cache reconciled)

SWR vs TanStack Query

Both solve server-state caching, but they make different trade-offs.

AspectSWRTanStack Query
Bundle size~4 KB, minimal APILarger, feature-rich
Mental modelKeys + fetcherQuery keys + query/mutation functions
Mutationsmutate (manual cache control)Dedicated useMutation with lifecycle hooks
DevtoolsBasic (community)First-class devtools
Pagination/infiniteuseSWRInfiniteBuilt-in useInfiniteQuery
Best forLightweight apps, simple read-heavy UIsComplex caching, heavy mutation workflows

Reach for SWR when you want the smallest footprint and mostly read data; reach for TanStack Query when you need granular cache control, structured mutations, and richer tooling.

Best Practices

  • Use stable, unique keys — the URL with its query params — so the cache and deduplication behave correctly.
  • Define your fetcher once and pass it via SWRConfig to avoid repeating it on every hook.
  • Prefer isLoading (first load only) over isValidating (any in-flight request) when deciding whether to show a spinner.
  • Use optimisticData with rollbackOnError for snappy writes that stay consistent on failure.
  • Gate dependent requests by passing null as the key instead of branching around the hook, which would violate the Rules of Hooks.
  • Tune refreshInterval and revalidateOnFocus per resource; aggressive polling on rarely-changing data wastes requests.
Last updated June 14, 2026
Was this helpful?