Skip to main content
How to Fetch Data in React with useSWR
18 min read

How to Fetch Data in React with useSWR

This article was last updated on July 19, 2024, to add sections for Error Handling, Mutations and Caching Strategies with SWR.

Introduction

Data is unquestionably an important component of any modern web application today. And, as the web evolves and innovates, new methods of interacting with this data must emerge in order to provide users with a better experience when interacting with our applications.

There have been a variety of technologies used for client-side data fetching. JavaScript, for example, has the native Fetch() API for data fetching. Axios is a promised-based HTTP client library for making asynchronous HTTP requests and fetching any data on the client-side.

SWR is one of the most powerful client-side data fetching libraries for frontend frameworks today. In this article, we will explore the features and benefits of using SWR in an example Next.js application, and provide a step-by-step guide for getting started with the library.

Whether you are new to React.js or an experienced developer looking to optimize your data fetching strategy, SWR is a powerful tool worth considering! Now, let's get started!

What is SWR and useSWR?

SWR is an acronym for stale-while-revalidate. It's a lightweight React.js library with hooks for data fetching on the client-side. SWR is bootstrapped with various hooks that are used for various performance improvement techniques such as data caching, re-validation, pagination, and many others.

The latest version of SWR, SWR 2.0, was released on December 9 and it features some new and exciting updates such as data preloading, new mutation APIs, improved optimistic UI capabilities, and a brand new DevTools for better developer experience.

useSWR, on the other hand is the most basic hook from the SWR library for data fetching. A basic example using the hook is shown below:

const { data, error } = useSWR(key, fetcher);

The useSWR hook accepts two arguments. One, a key value (usually the API endpoint to fetch data from), and a fetcher, which is an async function that contains the logic for fetching the data.

From the above example, useSWR returns two values: data, which is the value returned from the resource you're fetching, and a error, which contains error, if any is caught. It also returns two additional values, isLoading and isValidating, depending on the state of the fetcher function.

In the next section, we'll see how we can use this powerful hook as well as the other capabilities of SWR in an example application.

Setting up an Example App With SWR

We'll create an example application in React to get started with understanding how SWR works and how to start using it. Basically, we'll compare and contrast fetching data with the native 'Fetch() API' (or other client-side data fetching libraries such as Axios) you're used to and fetching data with SWR.

You can check out the complete code for the application on GitHub.

Run the following command in your terminal to create a new React project with create-react-app:

npx create-react-app my-swr

or with Yarn:

yarn create react-app my-swr

NOTE: I named the app my-swr for brevity but you can also choose any name you're okay with.

After the installation is complete, cd into the project directory and start the development server with npm start or yarn start.

Next, run the following command to install the necessary packages for our example application.

npm install styled-components swr

or with Yarn:

yarn add styled-components swr

Creating a Server

For the example application we'll be building, we'll set up a server with express running on http://localhost:3001. The server basically returns a list of users that will be rendered on the client-side.

In the project root folder, create a new server folder, navigate to it on the command line and run the following command to create a new Node.js project with an auto-generated package.json file:

npm init -y

or with Yarn:

yarn init -y

Next, we'll install the necessary packages to build up our server.

npm install express cors nodemon

with Yarn:

yarn add express cors nodemon

Still in the server folder, create a file named index.js with the following code:

server/index.js
const express = require("express");
const cors = require("cors");
const app = express();

app.use(cors());
const data = [
{
name: "Kim Doe",
age: 23,
avatar: "https://randomuser.me/api/portraits/women/44.jpg",
},
{
name: "Mary Jane",
age: 25,
avatar: "https://randomuser.me/api/portraits/women/50.jpg",
},
{
name: "Ken Joe",
age: 24,
avatar: "https://randomuser.me/api/portraits/women/18.jpg",
},
];

app.get("/", (req, res) => {
res.json(data);
});

app.listen(3001, () => {
console.log("App listening on port 3001");
});

In the above code, we're basically creating a server in express.js and also, we created a dummy data array for the list of users we'll render on the client-side. We also added cors to allow our client application communicate with the server without running into errors.

Next, we'll edit the content of server/package.json to the following:

server/package.json
{
"name": "server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
},
"scripts": {
"dev": "nodemon index.js"
}
}

NOTE: I have nodemon installed globally so it's not included in the list of dependencies, but you'll find it included in yours. Also, if you're using git you may want to create a .gitignore file in the server folder to avoid pushing the files in node_modules.

We can now run npm run dev or yarn dev (depending on the package manager you used) to start up the server. If everything works correctly you should see App listening on port 3001 logged to the console.

Data Fetching with Fetch() API

Let's go back to the client-side code now that we have the server up and running. In this part, we'll fetch the users from our backend (the server we created earlier) and display them in a simple component.

Navigate to the src folder in the root directory and create a folder named components. In the components folder create a Home.js file with the following code:

src/components/Home.js
import React from "react";
import { useState, useEffect } from "react";

const Home = () => {
const [users, setUsers] = useState(null);
useEffect(() => {
async function fetchUsers() {
const response = await fetch("http://localhost:3001");
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);

if (!users) return <h2>Loading...</h2>;
return (
<>
<div>
{users.map((user, index) => {
return <h2 key={index}>{user.name}</h2>;
})}
</div>
</>
);
};

export default Home;

Next, edit the content of src/App.js to the following:

src/App.js
import "./App.css";
import Home from "./components/Home";

function App() {
return (
<div className="App">
<header className="App-header">
<Home />
</header>
</div>
);
}

export default App;

In the code above, we're importing the Home.js component we created to render it in the homepage of our application. Now, if you navigate to http://localhost:3000, the list of users from the server should be displayed as shown below:

useswr

Our app works correctly. Awesome! Now, let's see how the application behaves when we add a new user to the user list on the server.

In the server/index.js file, add a new user to the data array like below:

server/index.js
...
const data = [
...
{
"name": "John Doe",
"age": 24,
"avatar": "https://randomuser.me/api/portraits/women/18.jpg"
}
]

Notice how the list isn't updated with the new user when we navigate back to http://localhost:3000. It's only when we reload the page before we get the updated list of users.

The GIF below shows this even better:

useswr

Inconsistencies in UI updates and data fetching behavior could be disastrous for applications that rely on real-time data to provide a reactive and seamless experience to users. This is one major problem that SWR aims to solve.

Data Fetching With SWR

In this part, we'll see how SWR outshines other data fetching methods by automatically re-validating and updating data without requiring us to manually refresh or reload the application.

Edit the content of src/components/Home.js to the following:

src/components/Home.js
import React from "react";
import useSWR from "swr";

const Home = () => {
const fetcher = (...args) => fetch(...args).then((res) => res.json());
const { data, error, isLoading } = useSWR("http://localhost:3001", fetcher);

if (error) return <div>Failed to fetch users.</div>;
if (isLoading) return <h2>Loading...</h2>;
return (
<>
<div>
{data.map((user, index) => {
return <h2 key={index}>{user.name}</h2>;
})}
</div>
</>
);
};

export default Home;

Let's go through the code above to understand what's going on.

From the code above, we import the useSWR hook from SWR. The hooks take a key, that is, the URL of our server and a fetcher function that contains the logic for fetching the data. The hook then returns three values based on the state of the result from the server.

The data object contains the list of users and the error will contain any error that is thrown. Next, we map through the users array that is returned in the data object and render them on the page.

Now, if you navigate to http://localhost:3000, the list of users should be displayed like below:

useswr

So far so good. Now, let's also add a new user to the list on the server and see how the application behaves. Open the server/index.js file and add a new user to the data array like so:

server/index.js
...
const data = [
...
{
"name": "Peter Pan",
"age": 24,
"avatar": "https://randomuser.me/api/portraits/women/21.jpg"
}
]

Go to http://localhost:3000 in the browser and note how the users list is automatically updated with the new user we added without us reloading the page.

The GIF below shows this in action.

useswr

Notice how fast and consistent the UI is updated with the new user!

This example shows one of the incredible powers of SWR over other client-side data fetching methods. It shows how SWR is able to auto re-validate and update the UI instantly using cached data and returning the latest user from the server.

How SWR is able to infer that the data has been updated and then re-validating and updating the UI is beyond the scope of this article. However, if you're curious to know, you can read more about it here in the docs.

Additional Features of SWR

We saw one of SWR's best features in action in the previous section; this section will show you even more of what SWR is made and capable of.

Pagination

SWR simplifies paginating data using the useSWR hook. We'll see a basic example of paginating data with useSWR using the Rick and Morty Character API.

In the components folder of our example application, create a file named Characters.js and add the following code to it:

src/components/Characters.js
import React from "react";
import { useState } from "react";
import useSWR from "swr";
import styled from "styled-components";

const Wrapper = styled.div`
margin: 50px auto;
`;
const Character = styled.div`
width: 250px;
height: 200px;
border-radius: 6px;
outline: none;
border: none;
`;

const Button = styled.button`
width: 120px;
height: 40px;
border-radius: 6px;
background-color: #6c6ce3;
margin-inline: 10px;
outline: none;
border: none;
color: white;
font-weight: bold;
`;

const Container = styled.div`
width: 100%;
margin: 0 auto;
display: grid;
gap: 10px;
padding-block: 20px;
grid-template-columns: 2fr 2fr 2fr 2fr;
`;

const Characters = () => {
const [pageIndex, setPageIndex] = useState(1);

const fetcher = (...args) => fetch(...args).then((res) => res.json());

const { data, error, isLoading } = useSWR(
`https://rickandmortyapi.com/api/character/?page=${pageIndex}`,
fetcher,
);

if (error) return <div>Failed to fetch characters.</div>;
if (isLoading) return <h2>Loading...</h2>;

return (
<>
<Container>
{data.results.map((character) => (
<Character key={character.id}>
<img width={100} height={100} src={character.image} />
<div>{character.name}</div>
</Character>
))}
</Container>
<Wrapper>
<Button onClick={() => setPageIndex(pageIndex - 1)}>Previous</Button>
<Button onClick={() => setPageIndex(pageIndex + 1)}>Next</Button>
</Wrapper>
</>
);
};

export default Characters;

In the above code, we are fetching characters from the Rick and Morty API. The endpoint returns 20 characters per request. We also used a React state to keep track of the pages so we can simply move forward and backward through the fetched characters to create a pagination effect.

Next, open the App.js file in the root folder and edit its content to this:

App.js
import "./App.css";
// import Home from './components/Home'
import Characters from "./components/Characters";

function App() {
return (
<div className="App">
<header className="App-header">
{/* <Home/> */}
<Characters />
</header>
</div>
);
}

export default App;

Basically, what we're doing is commenting out the previous Home component and then import and render the Characters component on the homepage instead.

Navigate to http://localhost:3000 and you should see the characters rendered as shown in the GIF below, with pagination enabled:

useswr

You can read more about pagination in SWR in the docs.

Preloading Data With SWR

Another notable use case of SWR is data preloading. With SWR, we can prefetch data, for example, fetching blog posts from a CMS or prefetching page routes in Next.js for faster and smooth client-side transitions.

We can prefetch data using the preload API from SWR. The following example from the docs shows how you can preload a user component on the click of a button:

import { useState } from 'react'
import useSWR, { preload } from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

// Preload the resource before rendering the User component below,
// this prevents potential waterfalls in your application.
// You can also start preloading when hovering the button or link, too.
preload('/api/user', fetcher)

function User() {
const { data } = useSWR('/api/user', fetcher)
...
}

export default function App() {
const [show, setShow] = useState(false)
return (
<div>
<button onClick={() => setShow(true)}>Show User</button>
{show ? <User /> : null}
</div>
)
}

Mutations with SWR

I wanted to share some details about handling mutations with SWR to help us efficiently manage data updates in our React applications. Here are some practical tips and examples:

SWR provides an excellent way to mutate data, which means updating or changing the cached data. This is useful for operations like creating, updating, or deleting records. Using the mutate function, we can optimistically update the UI and revalidate the data in the background.

Here’s a simple example of how to use mutate with SWR:

import useSWR, { mutate } from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

const MyComponent = () => {
const { data, error } = useSWR("/api/data", fetcher);

const handleUpdate = async () => {
// Optimistically update the data
mutate("/api/data", newData, false);

// Send the update request to the server
await fetch("/api/data", {
method: "POST",
body: JSON.stringify(newData),
});

// Revalidate the data
mutate("/api/data");
};

if (error) return <div>Failed to load data. Please try again later.</div>;
if (!data) return <div>Loading...</div>;

return (
<div>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={handleUpdate}>Update Data</button>
</div>
);
};

export default MyComponent;

In this example, we optimistically update the cached data before sending the update request to the server. After the server request completes, we revalidate the data to ensure it is up-to-date.

Handling Mutations

Optimistic UI Updates: SWR allows you to update the UI optimistically, which means reflecting the changes immediately without waiting for the server response. This provides a better user experience.

mutate("/api/data", newData, false); // Optimistically update data

Revalidation: After performing the mutation, you should revalidate the data to ensure consistency.

mutate("/api/data"); // Revalidate data

Error Handling: Handle errors gracefully to ensure the UI doesn’t break when there are issues with the mutation.

const handleUpdate = async () => {
try {
mutate("/api/data", newData, false);
await fetch("/api/data", {
method: "POST",
body: JSON.stringify(newData),
});
mutate("/api/data");
} catch (error) {
console.error("Failed to update data:", error);
}
};

Error Handling with SWR

I wanted to share some insights on handling errors gracefully when using SWR for data fetching in our React projects. Here are some strategies and examples that might help:

When using the useSWR hook, handling errors is straightforward. The hook returns an error object that we can use to display error messages or fallback content to the user.

Here's a simple example:

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

const MyComponent = () => {
const { data, error } = useSWR("/api/data", fetcher);

if (error) return <div>Failed to load data. Please try again later.</div>;
if (!data) return <div>Loading...</div>;

return (
<div>
{/* Render your data here */}
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};

export default MyComponent;

In this example, the component first checks if there's an error. If an error is detected, it displays a friendly error message. If there's no error but the data is still being fetched, it shows a loading indicator. Once the data is successfully fetched, it renders the data.

Tips for Handling Errors

Show User-Friendly Messages: Always provide a clear and concise error message that guides the user on what went wrong and possible actions they can take. Use a Fallback UI: In case of errors, provide a fallback UI so that the application doesn't break. This can be a simple message or a more sophisticated error boundary. Log Errors for Debugging: Use logging tools to capture errors for debugging purposes. This helps in understanding and fixing issues faster.

Implementing these strategies can make our applications more robust and improve the user experience.

SWRDevTools

The newest version of SWR comes with developer tools for debugging and writing your code more efficiently and confidently. You can install the SWRDevTools extensions for both Chrome and Firefox from their respective stores.

Be aware that SWRDevTools is not an official project of Vercel. You can learn more about the project from their website.

TypeScript Support

SWR also comes with full TypeScript support out of the box. If you're using TypeScript you can be rest assured of type safety in your application. For example, SWR will infer the argument types of fetcher from key, so you can have the preferred types automatically.

You can also explicitly specify the types for key and fetcher's arguments like so:

import useSWR, { Key, Fetcher } from "swr";

const uid: Key = "<user_id>";
const fetcher: Fetcher<User, string> = (id) => getUserById(id);

const { data } = useSWR(uid, fetcher);
// `data` will be `User | undefined`.

You can read more about using TypeScript with SWR here.

This section only touched on a few of SWR's many capabilities, so your first encounter with it will not be overwhelming. You can however, check the documentation for other features that might interest you.

Caching Strategies with SWR

I wanted to share some insights on caching strategies with SWR to improve the performance and user experience of our React applications. Here are some practical tips and examples that might help:

SWR automatically caches data to optimize performance and reduce the number of network requests. However, there are several ways to customize caching to suit specific needs.

Here's an example of basic caching with SWR:

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

const MyComponent = () => {
const { data, error } = useSWR("/api/data", fetcher);

if (error) return <div>Failed to load data. Please try again later.</div>;
if (!data) return <div>Loading...</div>;

return (
<div>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};

export default MyComponent;

In this example, SWR automatically caches the data fetched from the API.

Custom Caching Strategies

Cache Time Control: You can set a specific cache time for your data. This is useful if the data doesn't change frequently.

const { data, error } = useSWR("/api/data", fetcher, {
refreshInterval: 60000,
}); // Refresh every 60 seconds

Revalidation: SWR can revalidate the cache when the user focuses on the window or when network status changes. This ensures users always see the most up-to-date data.

const { data, error } = useSWR("/api/data", fetcher, {
revalidateOnFocus: true,
revalidateOnReconnect: true,
});

Manual Cache Management: Sometimes, you may want to manually manage the cache, especially when dealing with real-time data updates or invalidations.

import useSWR, { mutate } from "swr";

// Manually mutate the cache
mutate("/api/data", newData, false); // Update the cache without revalidation

Tips for Effective Caching

Set Reasonable Cache Intervals: Depending on how frequently your data changes, set appropriate cache refresh intervals. Use Conditional Fetching: Fetch data conditionally based on certain criteria to avoid unnecessary requests. Combine with Other State Management Libraries: SWR can be combined with other state management libraries like Redux or Zustand for more complex scenarios.

By implementing these caching strategies, we can ensure our applications are more efficient and provide a smoother experience for users.

Conclusion

In this article, we learned about the basics of SWR, its importance in developing modern web applications, and how you can get started with it. We also created an example application in React and explored some capabilities of SWR to see its use cases and benefits over other conventional client-side data fetching methods.

I hope you found this article useful and that you will begin using SWR to achieve the performance improvements you've always desired in your applications!