Skip to main content

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 fails
  • isLoading: true while the first request is in flight
  • mutate: 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 isLoading and error states
  • 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.

Further Reading