engineering

How to Fetch Data in React with useSWR

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

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:

  1. key: A unique key string that uniquely identifies a request. It could also be an array, function, or null.

  2. fetcher: A fetcher function that defines how to fetch the data. It takes key as its argument. Hence in the above code, the function will call /api/products API endpoint.

  3. options: An object of options for this particular hook. It is optional, and you can read more about it here.

It returns the following:

  1. data: API response for the given key, resolved by the fetcher function. It’s initially undefined.

  2. error: Error thrown by fetcher. It’s also initially undefined.

  3. isLoading: It’s true when there’s an ongoing request and data is not loaded.

  4. isValidating: If data is being invalidated, it’s set to true.

  5. mutate(data?, options?): A function that mutates cached data.

Get complete code for this part on my GitHub.

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 as useSWR's key.

  • 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:

  1. 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.

  2. 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.

Get complete code for this part on my GitHub.

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:

  1. Previous button decreases the page index by 1. If the current value pageIndex is 1, we disable it as we don’t want 0 or negative values for pageIndex.

  2. Next button increases the pageIndex by 1. It’s disabled if we have fetched all data.

Get complete code for this part of the blog on my GitHub.

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 returned

  • setSize: 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.

Get complete code for this part of the blog on my GitHub.

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 to false.

Revalidation in useSWR

useSWR hook offers several different approaches to keep data fresh and updated. Here’s a brief information about each approach:

  1. Revalidate on Focus: By default, useSWR revalidates the data when the window is focused.

  2. Revalidate if stale: Automatically revalidate even if there is stale data.

  3. Revalidate on Interval: You can set up periodic revalidation.

  4. Revalidate on Reconnect: useSWR can automatically revalidate when the user regains internet connection.

  5. 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! 👨‍💻

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.