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
useSWRis 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.
isLoadingistrueonly on the first load when there is no cached data yet.isValidatingistruewhenever a request is in flight, including background revalidations.- SWR deduplicates identical requests made within the
dedupingIntervalwindow (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
useSWRto type the return:useSWR<Data, Error>(key, fetcher). - The fetcher function's return type should match the
Datageneric. - 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[] | undefinedGotchas
- 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.okin your fetcher. dataisundefinedduring the initial load. Always handle the loading state or use optional chaining.- Calling
useSWRin a Server Component will error. SWR hooks are client-only.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| useSWR | Automatic caching, dedup, revalidation | Client-only, extra bundle size |
| fetch in useEffect | No dependencies | No caching, race conditions, boilerplate |
| React Query (TanStack) | Similar features, more built-in mutations | Larger API surface |
| Server Components fetch | Zero client JS, cached at server | No 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
nullas the SWR key prevents fetching entirely. This is the idiomatic way to conditionally fetch: whenshouldFetchis false, the URL isnulland SWR skips the request. keepPreviousData: trueshows the previous search results while a new query is in flight, avoiding a blank screen flash between searches.dedupingInterval: 5000prevents 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: falseandrevalidateOnReconnect: falsedisable automatic background revalidation. For search, stale results are acceptable and re-fetching on tab focus would be surprising to users.- The
useMemoon 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) withisValidating(background fetch) for a complete loading indicator.
FAQs
What is the difference between isLoading and isValidating in useSWR?
isLoadingistrueonly on the first load when there is no cached data.isValidatingistruewhenever a request is in flight, including background revalidations.- On subsequent revalidations,
isLoadingisfalsebutisValidatingistrue.
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[] | undefinedWhat 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.