React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrfetchinguseSWRkeys

Basic Data Fetching with SWR

Recipe

Use the useSWR hook with a key and a fetcher function to declaratively fetch, cache, and revalidate remote data in React components.

"use client";
 
import useSWR from "swr";
 
const fetcher = (url: string) => fetch(url).then((res) => res.json());
 
function Dashboard() {
  const { data, error, isLoading, isValidating } = useSWR("/api/dashboard", fetcher);
 
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
 
  return (
    <div>
      <h1>{data.title}</h1>
      {isValidating && <span>Refreshing...</span>}
    </div>
  );
}

Working Example

"use client";
 
import useSWR from "swr";
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
const fetcher = (url: string): Promise<User[]> =>
  fetch(url).then((res) => {
    if (!res.ok) throw new Error("Network response was not ok");
    return res.json();
  });
 
export default function UserList() {
  const { data: users, error, isLoading } = useSWR<User[]>("/api/users", fetcher);
 
  if (isLoading) return <div className="skeleton" />;
  if (error) return <div role="alert">Failed to load users</div>;
 
  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>
          {user.name} — {user.email}
        </li>
      ))}
    </ul>
  );
}

Deep Dive

How It Works

  • The first argument to useSWR is the key — a unique string (or array, or null) that identifies the request and drives the cache.
  • The second argument is the fetcher — any async function that receives the key and returns data.
  • SWR returns cached data immediately (stale), then revalidates in the background (while revalidate), and finally updates the UI with fresh data.
  • isLoading is true only on the first load when there is no cached data yet.
  • isValidating is true whenever a request is in flight, including background revalidations.
  • SWR deduplicates identical requests made within the dedupingInterval window (default: 2000ms).

Variations

Array keys (multi-argument fetcher):

const fetcher = ([url, token]: [string, string]) =>
  fetch(url, { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.json());
 
const { data } = useSWR(["/api/user", token], fetcher);

Using axios:

import axios from "axios";
 
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
 
const { data } = useSWR("/api/posts", fetcher);

GraphQL fetcher:

const fetcher = (query: string) =>
  fetch("/api/graphql", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query }),
  })
    .then((res) => res.json())
    .then((json) => json.data);
 
const { data } = useSWR("{ user { name } }", fetcher);

TypeScript Notes

  • Pass generics to useSWR to type the return: useSWR<Data, Error>(key, fetcher).
  • The fetcher function's return type should match the Data generic.
  • Key type can be string, [string, ...args], or a function returning either.
import useSWR, { Fetcher } from "swr";
 
const fetcher: Fetcher<User[], string> = (url) =>
  fetch(url).then((r) => r.json());
 
const { data } = useSWR<User[]>("/api/users", fetcher);
// data is User[] | undefined

Gotchas

  • The key must be a stable reference. Creating a new array literal inline on every render (e.g., useSWR([url, id], ...)) is fine because SWR serializes array keys internally, but object keys are not supported.
  • If your fetcher does not throw on HTTP errors, SWR will treat a 404 or 500 response as successful data. Always check res.ok in your fetcher.
  • data is undefined during the initial load. Always handle the loading state or use optional chaining.
  • Calling useSWR in a Server Component will error. SWR hooks are client-only.

Alternatives

ApproachProsCons
useSWRAutomatic caching, dedup, revalidationClient-only, extra bundle size
fetch in useEffectNo dependenciesNo caching, race conditions, boilerplate
React Query (TanStack)Similar features, more built-in mutationsLarger API surface
Server Components fetchZero client JS, cached at serverNo real-time updates

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: Search with SWR + debouncing
// File: src/hooks/use-search.ts
export function useSearch() {
  const [debouncedQuery, setDebouncedQuery] = useState(query);
 
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedQuery(query), 300);
    return () => clearTimeout(timer);
  }, [query]);
 
  const shouldFetch = debouncedQuery.length >= 3;
  const apiUrl = shouldFetch
    ? `/api/search?q=${encodeURIComponent(debouncedQuery)}&platform=${platform}`
    : null;
 
  const { data, error, isLoading, isValidating } = useSWR<SearchResponse>(
    apiUrl,
    fetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      dedupingInterval: 5000,
      keepPreviousData: true,
    }
  );
 
  return useMemo(() => ({
    results: data?.results,
    isLoading: isLoading || (shouldFetch && isValidating),
  }), [data, isLoading, shouldFetch, isValidating]);
}

What this demonstrates in production:

  • Passing null as the SWR key prevents fetching entirely. This is the idiomatic way to conditionally fetch: when shouldFetch is false, the URL is null and SWR skips the request.
  • keepPreviousData: true shows the previous search results while a new query is in flight, avoiding a blank screen flash between searches.
  • dedupingInterval: 5000 prevents duplicate requests for the same key within 5 seconds. If the user types "react", deletes it, and types "react" again within 5 seconds, SWR returns the cached result instead of hitting the API.
  • revalidateOnFocus: false and revalidateOnReconnect: false disable automatic background revalidation. For search, stale results are acceptable and re-fetching on tab focus would be surprising to users.
  • The useMemo on the return object prevents consumer components from re-rendering when the hook's internal state changes but the returned values have not changed.
  • The loading state combines isLoading (first load) with isValidating (background fetch) for a complete loading indicator.

FAQs

What is the difference between isLoading and isValidating in useSWR?
  • isLoading is true only on the first load when there is no cached data.
  • isValidating is true whenever a request is in flight, including background revalidations.
  • On subsequent revalidations, isLoading is false but isValidating is true.
Can I use an object as a key for useSWR?

No. Object keys are not supported because SWR cannot serialize them reliably. Use strings or arrays instead. Array keys are serialized internally, so ["/api/users", id] works fine.

How does SWR deduplicate identical requests?

When multiple components call useSWR with the same key within the dedupingInterval window (default 2000ms), only one network request is made. All hooks receive the same promise result.

Gotcha: What happens if my fetcher does not check res.ok before returning?

SWR will treat 404 and 500 responses as successful data. The error field will remain undefined. Always validate res.ok and throw an error for non-2xx responses.

Can I use useSWR inside a React Server Component?

No. useSWR is a React hook that requires client-side rendering. Calling it in a Server Component will throw an error. Use fetch directly in Server Components and pass data as props.

How do I use array keys with a multi-argument fetcher?
const fetcher = ([url, token]: [string, string]) =>
  fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
  }).then((r) => r.json());
 
const { data } = useSWR(["/api/user", token], fetcher);
What does keepPreviousData do in SWR?

It preserves the previous data while a new request is in flight (e.g., when the key changes). This avoids a blank screen flash between key transitions, which is especially useful for search UIs.

How do I properly type useSWR with TypeScript generics?
import useSWR, { Fetcher } from "swr";
 
const fetcher: Fetcher<User[], string> = (url) =>
  fetch(url).then((r) => r.json());
 
const { data } = useSWR<User[]>("/api/users", fetcher);
// data is User[] | undefined
What is the Fetcher type exported from SWR used for in TypeScript?

Fetcher<Data, Key> enforces that the fetcher accepts the key type as input and returns Promise<Data>. It provides compile-time type safety between the key, fetcher, and returned data.

Gotcha: Why is data undefined even after the component mounts?

data is always undefined during the initial load before the fetcher resolves. Always handle the loading state with isLoading or use optional chaining (data?.field) to avoid runtime errors.

How does the stale-while-revalidate pattern work in practice?
  • SWR returns cached (stale) data immediately on render.
  • It fires a background revalidation request.
  • When fresh data arrives, it updates the UI seamlessly.
  • The user sees content instantly while the data silently refreshes.