How to Fetch Data with SWR in React
SWR (stale-while-revalidate) is Vercel's minimalist data-fetching hook for React that automatically caches, revalidates, and deduplicates requests. A fetcher is a simple async function that accepts a URL and returns parsed JSON; SWR wraps it with intelligent caching so you never over-fetch the same data. This guide walks you through creating your first SWR fetcher, handling loading and error states gracefully, and triggering revalidations when your user modifies data.
Installing and Configuring SWR
Install SWR via npm or yarn:
npm install swr
Create a fetcher function—the simplest version uses fetch and .json():
const fetcher = (url) => fetch(url).then((res) => res.json());
For POST requests or custom headers (e.g., authentication), extend the fetcher:
const fetcher = async (url) => {
const res = await fetch(url, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
const error = new Error('Failed to fetch');
error.status = res.status;
throw error;
}
return res.json();
};
Your First useSWR Hook
The useSWR hook takes a key (URL or array for multiple args) and a fetcher:
import useSWR from 'swr';
export function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {data.name}!</div>;
}
The hook returns an object with:
data: the fetched payload (undefined until loaded)error: thrown if fetch failsisLoading: true while the first request is in flightmutate: trigger revalidation on-demand
SWR automatically deduplicates requests—if two components fetch the same URL within milliseconds, only one HTTP request fires.
Handling Errors Gracefully
SWR treats any thrown error from your fetcher as a failure. Provide a fallback UI:
const { data, error, isLoading, isValidating } = useSWR(
'/api/posts',
fetcher,
{ revalidateOnFocus: false } // Disable refresh on window focus
);
if (error) {
return (
<div className="error">
<p>Failed to load posts: {error.message}</p>
<button onClick={() => mutate()}>Try again</button>
</div>
);
}
The isValidating flag is true during background revalidations—useful for showing "refreshing" spinners without blocking the UI.
Conditional Fetching
Don't fetch until a condition is met. Pause the fetcher by returning null as the key:
export function SearchUsers({ query }) {
const { data, isLoading } = useSWR(
query ? `/api/search?q=${query}` : null,
fetcher
);
return (
<div>
<input
type="text"
placeholder="Search users..."
onChange={(e) => setQuery(e.target.value)}
/>
{isLoading && <div>Searching...</div>}
{data && <UserList users={data} />}
</div>
);
}
Manual Revalidation
Call mutate() to revalidate immediately (e.g., after a form submission):
export function CreatePost() {
const { mutate } = useSWR('/api/posts', fetcher);
async function handleSubmit(e) {
e.preventDefault();
const newPost = { title: e.target.title.value };
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost)
});
mutate(); // Refresh the posts list
}
return <form onSubmit={handleSubmit}>...</form>;
}
You can also pass optimistic data to mutate:
const newPost = { id: 999, title: 'My Post', createdAt: new Date() };
mutate((oldData) => [...(oldData || []), newPost], false);
The second parameter false skips revalidation; set it true or omit it to fetch after.
Configuration and Revalidation Strategy
SWR's config object fine-tunes behavior:
const { data, error, isLoading } = useSWR(
'/api/users',
fetcher,
{
revalidateOnFocus: true, // Refresh when window regains focus
revalidateOnReconnect: true, // Refresh when network reconnects
dedupingInterval: 2000, // Don't dedupe if >2s elapsed
focusThrottleInterval: 5000, // Throttle focus revalidations to 5s max
errorRetryCount: 3, // Retry failed requests 3 times
errorRetryInterval: 5000, // Wait 5s between retries
}
);
For a chat app where stale data is unacceptable, enable aggressive revalidation. For a static dashboard, disable focus revalidation to save bandwidth.
Dependent Requests
Fetch a second endpoint only after the first completes. Chain useSWR calls conditionally:
export function UserPosts({ userId }) {
const { data: user } = useSWR(
userId ? `/api/users/${userId}` : null,
fetcher
);
const { data: posts } = useSWR(
user ? `/api/users/${userId}/posts` : null,
fetcher
);
return <PostList posts={posts} />;
}
Pagination Example
Implement infinite-scroll pagination by managing keys in state:
export function InfinitePostList() {
const [page, setPage] = useState(0);
const { data: posts = [], isLoading } = useSWR(
`/api/posts?page=${page}`,
fetcher
);
return (
<div>
<PostList posts={posts} />
<button onClick={() => setPage(page + 1)} disabled={isLoading}>
Load more
</button>
</div>
);
}
For true infinite-scroll with accumulated results, use SWR's useSWRInfinite hook.
Key Takeaways
- SWR is a lightweight hook that fetches, caches, and revalidates automatically
- Always provide a fetcher and handle
isLoadinganderrorstates - Use
mutate()to trigger revalidation after mutations - Conditional keys (returning
null) pause fetching until conditions are met - Configure revalidation strategy to match your app's data freshness needs
Frequently Asked Questions
What's the difference between isLoading and isValidating?
isLoading is true only on the initial request. isValidating is true during any background revalidation (including refetches triggered by focus, reconnect, or mutate()). Use isLoading for skeleton UIs; use isValidating for subtle spinners.
Can I use SWR with POST, PUT, or DELETE?
SWR is designed for GET requests. For mutations, manually fetch() and then call mutate() to revalidate, or use SWR's useSWRMutation (React Hook Form integration).
How do I add authentication headers?
Define your fetcher with the Authorization header:
const fetcher = (url) => fetch(url, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
}).then(r => r.json());
Does SWR work with GraphQL?
Yes. Pass a GraphQL endpoint as the URL and post a query in your fetcher:
const fetcher = (query) => fetch('https://graphql.api/graphql', {
method: 'POST',
body: JSON.stringify({ query })
}).then(r => r.json());
What happens if my fetcher throws an error?
SWR catches it and stores it in the error property. Your component can then render an error UI or retry.