
How to Fetch Data in React with useSWR
Learn to efficiently fetch and manage data in React using SWR. Explore caching, real-time updates, and error handling for optimized API requests

Kaushal Joshi
Oct 08, 2024 • 11 min read
Data fetching has always been an important part of any frontend application. It’s important to choose the right method that fulfills the needs, requirements and fits within the scope of your application.
In this blog, let’s dive deep into a library that works like a Swiss army knife for data fetching. I am talking about Vercel’s useSWR
library in React.
Pre-requirements
This is a beginner-friendly article that aims to briefly introduce major features of the useSWR
library in React. It’s fine if you have never worked with this library.
However, prior knowledge of React is a must. You must be familiar with basic React concepts like functional components, JSX, states, props, etc.
Furthermore, the demo app is built with Next.js and uses TypeScript, Tailwind CSS, and Next.js API route handlers. It’s absolutely not necessary to know them. It’s for demo purposes, and you can use it with any other React-based framework.
With that in mind, let’s get our IDE dirty…
What is useSWR
Hook in React?
The useSWR
hook provides an elegant way to fetch data effortlessly. It’s an open-source library from Vercel that offers tons of features out-of-the-box that make it stand out compared to traditional data-fetching approaches.
SWR stands for “Stale-While-Revalidate”. It is a fundamental cache invalidation strategy in HTTP. This strategy involves initially returning the cached stale data and then fetching the latest data in the background. Once the data is updated, the cache is updated as well and the user is presented with the latest data.
How does SWR stand out from other solutions?
There are plenty of popular options already used by many developers, including native fetch()
, Axios, just to name a few. However, these traditional methods come with certain drawbacks:
Boilerplate Code: They often require a significant amount of repetitive code to handle data fetching.
Manual Handling of API Responses: Developers need to manually manage loading states, error handling, and other aspects of the data-fetching lifecycle.
Lack of Built-In Caching: These solutions do not provide built-in caching mechanisms, which can lead to redundant network requests and decreased performance.
This is what makes useSWR
a blessing to use:
Fast, Lightweight, and Reusable Data Fetching: With just a few lines of code, you can handle data fetching, caching, loading and error states, etc.
Built-In Cache and Request Deduplication: This is one of the most important features of useSWR. Data is automatically cached, and subsequent requests for the same resource are duplicated. That means, that if multiple React components request the same data, it will only be fetched once, greatly improving performance.
Transport and Protocol Agnostic: While traditional solutions like fetch and Axios are HTTP-focused, useSWR hook is transport-agnostic. That means, it can handle any data source like REST, GraphQL, WebSockets, etc. as long as you provide the correct fetcher function (We will talk more about the fetcher function soon).
SSR / ISR / SSG support: SWR hook provides support for the three most important server side rendering methods.
TypeScript ready: The whole library is typefaced, so you can use all TypeScript features with no efforts
Works Seamlessly in React Native: Beyond the web, useSWR integrates smoothly with React Native, offering a unified data-fetching solution across web and mobile platforms. It even includes advanced mobile-first features like offline support.
Installing and Basic Usage of useSWR
Inside your React project, install swr
as a dependency.
# NPM
npm i swr
# YARN
yarn add swr
# PNPM
pnpm add swr
Now you can import the useSWR
hook like any other library:
import useSWR from 'swr';
Note: As
useSWR
is a hook, it can only be imported in client components. You cannot import it React Server Components.
Basic Uses
If you are fetching data from a RESTful API that returns JSON data, first you need to create a fetcher
function. This function is a wrapper around the native fetch()
.
const fetcher = (url: string) => fetch(url).then((res) => res.json());
Then you can use it like this:
import useSWR from 'swr';
const BasicUsage = () => {
const { data, error, isLoading } = useSWR('/api/products', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className='flex flex-col items-center justify-center gap-8'>
<h1 className='font-semibold text-center text-2xl'>Fetched Data:</h1>
<Table products={data.products} />
</div>
);
};
export default BasicUsage;
The useSWR
hook takes three parameters:
key
: A unique key string that uniquely identifies a request. It could also be an array, function, or null.fetcher
: A fetcher function that defines how to fetch the data. It takeskey
as its argument. Hence in the above code, the function will call/api/products
API endpoint.options
: An object of options for this particular hook. It is optional, and you can read more about it here.
It returns the following:
data
: API response for the given key, resolved by thefetcher
function. It’s initiallyundefined
.error
: Error thrown byfetcher
. It’s also initiallyundefined
.isLoading
: It’s true when there’s an ongoing request and data is not loaded.isValidating
: If data is being invalidated, it’s set to true.mutate(data?, options?)
: A function that mutates cached data.
Mutation with SWR
In SWR, mutation is the ability to modify or update the cached data and then optionally revalidate it. This is useful when performing actions like creating, updating, or deleting resources on the server and ensuring the UI reflects the most up-to-date data immediately on the screen without waiting for the server's response.
Here is the basic syntax:
mutate(key, data, options);
key
: same asuseSWR
'skey
.data
: data to update the client cache, or an async function for the remote mutation.options
: Options related to mutation. Know more about various options here.
Let’s modify our code a bit. We want to increase the price of a particular product by 10. Let’s create a function updateProduct
that takes a product as an argument.
const updateProduct = async (product: Product) => {
mutate(
`/api/products/${product.id}`,
{ ...product, price: product.price + 10 },
false
);
const res = await fetch(`/api/products/${product.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ price: product.price + 10 }),
});
const data = await res.json();
alert(`Updated data: ${JSON.stringify(data)}`);
// Revalidate the data after mutation
mutate(`/api/products/${product.id}`);
};
As we are working with mock data, nothing won’t change on the screen. Check the updated price on the alert box. Now let’s understand what we did:
Optimistic update: The
mutate()
is called before the server request starts. This updates the product’s price immediately in the UI. The third parameter,false
, doesn’t trigger a revalidate function immediately.Revalidation: After sending the PUT request,
mutate
is called again with the same API route to revalidate and fetch the latest data from the server.
Pagination with SWR
Handling pagination is so simple with SWR. Let’s modify the previous code:
const Pagination = () => {
const [pageIndex, setPageIndex] = useState(1);
const { data, error, isLoading } = useSWR(
`/api/products?page=${pageIndex}`,
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data.</div>;
return (
<div>
<h1>Pagination Data:</h1>
<Table products={data.products} />
<div>
<button
onClick={() => setPageIndex((prev: number) => prev - 1)}
disabled={pageIndex === 1}
>
Previous
</button>
<button
onClick={() => setPageIndex((prev: number) => prev + 1)}
disabled={pageIndex === data.totalPages}
>
Next
</button>
</div>
</div>
);
};
export default Pagination;
We declare a state pageIndex
that stores the current page index for pagination. We initialize it with 1
.
Then we provide two buttons for navigation:
Previous
button decreases the page index by 1. If the current valuepageIndex
is 1, we disable it as we don’t want 0 or negative values forpageIndex
.Next
button increases thepageIndex
by 1. It’s disabled if we have fetched all data.
Infinite loading with useSWR
Sometimes we want to build an infinite loading UI, with a Load More
button that appends data to the list. SWR provides useSWRInfinite
hook that enables us to trigger several requests without much hassle.
Here’s the syntax:
import useSWRInfinite from 'swr/infinite';
// Inside a component
const { data, error, isLoading, isValidating, mutate, size, setSize } =
useSWRInfinite(getKey, fetcher, options);
This hook is very similar to useSWR
. It accepts a function that returns the same values as the useSWR
hook, and additional 2 extra values:
size
: The number of pages that will be fetched and returnedsetSize
: A function that sets the number of pages to be fetched.
Let’s write some code to understand this better. For infinite loading with a load more button, we need to define a function that gets the key for each page.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return `/api/products?page=${pageIndex + 1}`;
};
The getKey
function accepts the index of the current page, as well as the data from the previous page. As pageIndex
is zero-based, we add 1
to the index.
The return value of this function will be accepted by the fetcher()
function in useSWRInfinite
. If null
is returned, then it’s considered that we’ve reached the end of our data, and the request won’t be triggered.
const InfiniteLoadingPage = () => {
const { data, isLoading, error, size, setSize } = useSWRInfinite<APIResponse>(
getKey,
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data.</div>;
if (!data) return null;
// Flatten the array of product arrays
const products = data.flatMap((page) => page.products);
return (
<div>
<p>{products.length} products listed</p>
<Table products={products} />
{products.length === data[0].totalProducts && (
<button onClick={() => setSize(size + 1)}>Load More</button>
)}
</div>
);
};
export default InfiniteLoadingPage;
The Load More
button increases the page size by one, hence fetching data for the next page.
Since useSWRInfinite
returns an array of pages, and each page contains an array of products. Therefore, we use flatMap()
to flatten all the products into a single array.
Error handling
useSWR provides robust error handling out of the box. When an error occurs during fetching, it's captured in the error
property returned by the hook.
Basic error handling
We have been handling errors in our previous examples in very simple manner.
if (error) return <div>Error loading data.</div>;
Here, you can render a dedicated error component with custom UI and messages, redirect to some pages, etc.
Retrying API calls on error
SWR provides a way to retry the API call in case an error occurs. We need to pass onErrorRetry
option with useSWR
hook. It gives you the flexibility to retry based on various conditions.
useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Never retry on 404.
if (error.status === 404) return;
// Only retry up to 5times.
if (retryCount >= 5) return;
// Retry after 5 seconds.
setTimeout(() => revalidate({ retryCount }), 5000);
},
});
First, it checks if the error’s status code is
404
. If so, it returns from the function, hence not triggering the API call again.SWR retries the request automatically if error occurs. In the above snippet, if it has tried and failed five times, it stops trying and returns from the function.
Finally, if none of the above conditions are met, the code will wait 5 seconds before retrying the request again.
Note: You can disable this it by setting
shouldRetryOnError
tofalse
.
Revalidation in useSWR
useSWR hook offers several different approaches to keep data fresh and updated. Here’s a brief information about each approach:
Revalidate on Focus: By default, useSWR revalidates the data when the window is focused.
Revalidate if stale: Automatically revalidate even if there is stale data.
Revalidate on Interval: You can set up periodic revalidation.
Revalidate on Reconnect: useSWR can automatically revalidate when the user regains internet connection.
Manual Revalidation: You can manually trigger revalidation using the
mutate
function.
Here’s how you can use them:
useSWR('/api/todos', fetcher, {
revalidateIfStale: false,
refreshInterval: data ? 5000 : 0, // Will revalidate after 5 seconds only if data is present
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshWhenOffline: true,
});
Wrapping up
SWR is a powerful tool that simplifies data fetching, caching, and error handling in React apps. Its automatic revalidation, caching, and mutation capabilities make it a must-have for optimizing data-fetching workflows.
I hope you found this blog helpful and learned the ins and outs of data fetching using SWR in React. If you did, do share this blog with your peers and colleagues. Also, feel free to reach out to me if you want to discuss data fetching in more detail. I am most active on Peerlist and Twitter.
And hey, if you're on the lookout for a Frontend Developer Job, check out Peerlist Jobs for some great opportunities.
Until next time, happy coding! 👨💻