Fetching, caching and revalidating server-side data with SWR
Emmanuel Fordjour Kumah
Posted on September 19, 2023
In this tutorial, you will learn how to use SWR to fetch data in a React application. SWR is a library for data fetching, revalidating, and caching.
By the end of the article you will know:
What is SWR?
The problem SWR solves?
How to set up SWR in your React app
How to use SWR to fetch data in a React component
How to use SWR for Pagination, auto revalidation, caching, etc.
Prerequisites
Before you begin you should be familiar with:
Basic understanding of JavaScript concepts such as the fetch method
React hooks
React components
What is SWR?
SWR is a React Hooks for data fetching. It is derived from stale-while-revalidate
an HTTP cache-control
mechanism that allows a web browser to cache a response for a resource on a server and store a copy of the response in the browser's local storage.
This allows the browser to display the cached response to the user without further request to the server. Periodically, the browser revalidates the cache to ensure responses are still up-to-date.
There is a Cache-Control
header returned by the server to specify the duration the browser should use the cache while in the background revalidating the data with the server. As a result, improving the performance of a web app
The strategy behind SWR is:
Display the cached response to the client for a specified duration( in seconds)
Revalidates the data (i.e. request fresh data) asynchronously while still displaying the stale data.
Get the up-to-date data and display the response to the user.
What problem does SWR solve?
The Axios library and the fetch
APIs are great options used to request resources on a server. However, whenever you initiate a request for a resource, there is a waiting period for the response.
In a React app, you generally handle this waiting period with the loading state or loading animation on the UI. When you get the response, you will use the set
function to update the state of your app, which triggers a re-render of your components.
There are several issues with this approach:
Whenever the component is mounted, you will need to make another request to the API endpoint for data.
When a response is returned by the server, you will need to perform the extra task of caching and paginating data as
fetch
andaxios
does not automate caching and pagination
To solve these challenges, you will use a React hook called useSWR
. It provides an easier approach to fetch data from a server, cache , and paginate it.
Why Do You Need To Use SWR?
Below are some benefits of using SWR in your React app:
Ease of use: SWR provides a straightforward API that makes it easy to fetch data from a server and cache it.
Automatic caching: SWR automatically caches data in the browser's local storage and displays the cached response to the user. This reduces the number of requests made to the server for a resource. As a result, it improves the performance of your app.
Auto-revalidation: SWR ensures users view the most up-to-date data. Regardless of the number of tabs opened, the data will always be in sync when the tab is refocused.
Stale-while-revalidate (SWR) caching strategy: SWR uses a stale-while-revalidate (SWR) caching strategy, which means that the cached data is used even after it has expired until the new data has been fetched from the server. This ensures that the user never sees stale data.
Error handling: SWR handles errors neatly. If the request fails, the user is not presented with an error message.
SWR has various hooks that can be used to improve the performance of your app. The most basic hook is the useSWR
which can be used for data fetching.
A basic example using the useSWR
hook is shown below:
const { data, error } = useSWR(key, fetcher);
Data fetching with fetch
vs useSWR
Let's use the fetch
API to fetch data and compare it to how to fetch data using useSWR
Fetching data with the fetch
API
Generally, to fetch data using the in-built JavaScript fetch
method in your React app, you will follow these steps:
Import
useEffect
anduseState
hooks from ReactFetch the data once using the
useEffect
in the parent component.In the callback function of the
useEffect()
, call thefetch()
method and pass the API endpoint as the argument.Update the
state
variable with the data.Now, all child components can access the fetched data when passed as props
The code snippet below fetches a list of products using the fetch
method and pass the data as props
in the Products
component.
import { useEffect, useState } from "react";
import Products from "./Products";
export default function App() {
const [products, setProducts] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
fetch("https://fakestoreapi.com/products")
.then((res) => res.json())
.then((data) => {
setIsLoading(false);
setProducts(data);
})
.catch((err) => setError(err));
}, []);
//check if data has been fetched
console.log(products);
//check if error
console.log(error);
return (
<div className="App">
{isLoading && <h2>Fetching data... </h2>}
<Products products={products} />
</div>
);
}
There are some issues with this approach:
There is too much code to write just to handle data fetching. This code will be difficult to maintain if we keep adding data dependency to the page
You will need to keep all the data fetching in the parent component and pass it down to child components via
props
You will need to manage a lot of
state
: Loading, error, and the fetched data. Whenever the state is updated, the parent component as well as the child components need to re-render to reflect the changes. This unnecessary re-rendering decreases the performance of the application.
Now, let's see how to fetch the same data using SWR. You can find the code snippet on the data-fetching branch on the GitHub repo.
Follow these steps to fetch data with the useSWR
hook :
Inside your React app run the command:
npm i swr
. This will install SWR in your React app.Navigate to the component to fetch data (eg.
<App/>
). Typeimport useSWR from "swr"
at the top of the component.Within the
<App/>
component, create afetcher
function. Thefetcher
is an async function that accepts the API endpoint as a parameter to fetch the data.In the body of the
fetcher
function , you can use any library likefetch
andaxios
to handle data fetching.
Below is the code snippet for the fetcher function
//fetch data using fetch api
const fetcher = url => fetch(url).then(res => res.json())
-
In the body of your top-level component, type the code below
//App.js const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
-
At the right hand side of the expression, we call the
useSWR()
hook. It accepts the following arguments- The
key
is a unique string representing the URL to the API endpoint. - The
fetcher
will pass thekey
to thefetcher
function and then proceed to fetch the data. -
option
is an object of options to help with data fetching
- The
-
-
On the left hand side of the expression , we use the array
destructuring
to extract the fetcheddata
,error
, andisLoading
andmutate
response.const { data, error, isLoading, mutate } = useSWR(key, fetcher, options)
Now that you have access to the
data
, you can use it in the required component. In this example, we are passing thedata
to the<Products/>
component.
The code snippet to fetch data from the API is as below:
import "./App.css";
import Products from "./components/Products/Products";
import useSWR from "swr";
//function to fetch data
const fetcher = (...args) => fetch(...args).then((res) => res.json());
//api endpoint
const url = "https://fakestoreapi.com/products";
function App() {
const { data, error, isLoading } = useSWR(url, fetcher);
if (error) return <p>Error loading data</p>;
if (isLoading) return <div>loading...</div>;
return (
<>
<main>
<h1>Data fetching with useSWR </h1>
<Products data={data} />
</main>
</>
);
}
export default App;
Now that the Products
component has access to the data
, we can use it to display the individual products.
Calling the useSWR()
method returns the following:
data
: This represents the response or result returned by thefetcher
functionerror
: error that is thrown by thefetcher
if unable to fetch the dataisLoading
: This helps you display a loading state if there's an ongoing request.isValidating
: If there is a request or revalidation loadingmutate
: This is a function to modify the cached data.
Using useSWR
hook :
It reduces the amount of code to write to fetch data resulting in clean and maintainable code
No need to manage a lot of
state
andside effects
withuseState
anduseEffect
hooks
In the next section, we will use useSWR
hook to paginate data.
Handling Pagination with SWR
To handle pagination in React, you will generally use a react-paginate
package or any suitable alternative. However, the useSWR
hook automatically handles pagination.
In this section, we will demonstrate pagination using the Rick and Morty Character API.
You can find the code in this pagination branch repo.
Below is the final version of the application
Let's get started:
Create a "Characters" sub-folder in the "Component" folder of your React app
Create a
Characters.jsx
andSingleCharacter.jsx
files inside the "Characters" folder
Type the code below into your Characters.jsx
file
//component/characters/Characters.jsx
import React, { useState } from "react";
import useSWR from "swr";
import SingleCharacter from "./SingleCharacter";
import "../../../App.css";
const Characters = () => {
const [pageIndex, setPageIndex] = useState(1);
const fetcher = (...args) => fetch(...args).then((res) => res.json());
// The API URL includes the page index, which is a React state.
const { data, error, isLoading } = useSWR(
`https://rickandmortyapi.com/api/character/?page=${pageIndex}`,
fetcher
);
if (error) return <h3>Failed to fetch characters</h3>;
if (isLoading) return <h3>Loading characters...</h3>;
return (
<div>
<section className="character_card">
{data.results.map((character) => (
<div className="character_card__item">
<SingleCharacter key={character.id} character={character} />
</div>
))}
</section>
<div>
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button
className="character_btn"
onClick={() => setPageIndex(pageIndex + 1)}
>
Next
</button>
</div>
</div>
);
};
export default Characters;
In the code snippet we:
Import the
useSWR from "swr"
anduseState from "react"
useState
helps us to keep track of the page number to be used to navigate to the next page or previous page.useSWR
enables us to fetch data from the API endpoint. It accepts the API endpoint andfetcher
functions as arguments. The endpoint will return 20 characters per request.The response from the API is assigned to the
data
.Finally, we iterate over each item in the array and display a single character with the
SingleCharacter.jsx
component.
To handle pagination:
We defined the "next" and "previous" buttons in the
<App/>
componentWhenever you click on the "next" button, we update the
pageIndex
(setState(pageIndex + 1)
). This initiates another request to the API endpoint for the next page.Similarly, whenever you click on the "previous" button, we decrease the
pageIndex
(setState(pageIndex -1 )
.
//code snippet for previous and next page
...
<div>
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button
className="character_btn"
onClick={() => setPageIndex(pageIndex + 1)}
>
Next
</button>
</div>
...
In the next section, you will learn how to prefetch data in SWR
Prefetching Data
SWR can be used to preload data before rendering a component. It has preload
API to prefetch resources and store the results in the cache. With that, all incoming requests for the same URL will reload the cached data. This prevents waterfalls in your application (waterfalls occur when data takes a long time to load, slowing down your applications).
For instance, you can preload all comments to a post from a CMS. Whenever the user clicks on the "show comments" button it displays the cached data, resulting in a faster and smoother user experience.
The preload
accepts key
and fetcher
as the arguments. You can call preload
even outside of React.
Below is the syntax for the preload
API:
preload(apiEndPoint, fetcher)
The code for this section is in the preload branch of the GitHub repo
Below is the code snippet to preload comments
import useSWR, { preload } from "swr";
const fetcher = (url) => fetch(url).then((res) => res.json());
// Preload the resource before rendering the Comments component below,
// this prevents potential waterfalls in your application.
preload("https://jsonplaceholder.typicode.com/posts/1/comments", fetcher);
const Comments = () => {
const { data, isLoading, error } = useSWR(
"https://jsonplaceholder.typicode.com/posts/1/comments",
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data</div>;
return (
<div>
{data.map((comment) => (
<div className="comments_card" key={comment.id}>
<h3>{comment.body}</h3>
<p>
<span>author</span>
{comment.name}
</p>
</div>
))}
</div>
);
};
export default Comments;
Auto Revalidation
Revalidation involves clearing the cache data in the local storage and retrieving the latest data from the server.
A use case is when data changes in your app and you want to ensure you display the most up-to-date data.
There are several approaches to revalidating data:
Revalidate on focus
Revalidate on interval
Revalidate on reconnect
Revalidate on Focus
This automatically revalidates the data to immediately sync to the latest state whenever you switch between tabs or re-focus a page. This is helpful for refreshing data when the laptop "goes to sleep", or a tab is suspended.
In the screenshot below, you will notice that in the "network" section of the DevTools, whenever you switch between tabs, a request is made to revalidate the data and fetch the latest data (if any)
With the example below, whenever the page is refocused, a network request is made to automatically revalidate the daa to ensure the latest data is displayed to the user.
(Demonstrating revalidation of data when the tap is suspended)
Revalidate on interval
Whenever you have multiple tabs opened, for the same application, the data for each tab may vary. To keep all the tabs up-to-date, SWR provides the option to automatically re-fetch data at an interval. The re-fetching happens only when the component associated with the hook is on screen.
For instance, you have opened a Todo App on multiple tabs, and on the current tab, you added a new todo. Whenever the item is added, both tabs will eventually render the same data even though the action happened on the active tab.
To enable this feature, call the useSWR()
method, pass the refreshInterval
as an object of option
and set refreshInterval
value in milliseconds
The syntax is as below:
useSWR('/api/todos', fetcher, { refreshInterval: 1000 })
-
{ refreshInterval: 1000 }
tells SWR to refresh the data every 1 second.
Revalidate on reconnect
In a scenario where the computer has lost connection to the internet, the data can be revalidated when the network connection is restored.
In the screenshot below, the network is offline, and when it is restored, SWR initiate a request to the API to fetch the latest data.
Automatic Caching
A key benefit of SWR is to automatically cached data from the server and display the response from the local storage.
You can use the DevTools to verfiy if a network request is cached:
-
Check the Size column in the Network tab. If the "Size" column says
disk cache
orMemory cache
, then the response was served from the cache.
Global Configuration with SWR
Whenever the same fetcher
function is needed to fetch resources, you can use the context SWRConfig
to provide global configuration for all SWR hooks. The SWRConfig
servers as a wrapper component allowing all child components to access the value assigned to the SWRConfig
.
The syntax is as below:
<SWRConfig value={options}> //options to be available to all components
<Component/>
</SWRConfig>
In the example below, all SWR hooks can use the same fetcher
function to load JSON
data from an API endpoint.
//main.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import { SWRConfig } from 'swr'
import App from './App'
import './index.css'
//common fetcher function
const fetcher = (...args) => fetch(...args).then((res) => res.json())
ReactDOM.render(
<React.StrictMode>
<SWRConfig value={{ fetcher: fetcher }}>
<App />
</SWRConfig>
</React.StrictMode>,
document.getElementById('root')
)
// App.js
import useSWR, { SWRConfig } from 'swr'
//no fetcher function needed here
function App () {
const { data: events } = useSWR('/api/events')
const { data: projects } = useSWR('/api/projects')
const { data: user } = useSWR('/api/user')
}
Instead of declaring the fetcher
function in every file, we created it in the main.jsx
and passed it as a value to SWRConfig
. Now, the App.js
and all its child components can fetch data without the need to create the fetcher
function again to avoid redundancy.
Error handling and revalidation
If an error is thrown in the fetcher
, it will be returned as error
by the hook.
The code snippet is as below:
const fetcher = url => fetch(url).then(res => res.json())
// ...
const { data, error } = useSWR('/api/user', fetcher)
if(error) return <div>Error loading data </div>
In the event of an error, the last successfully generated data will continue to be served from the cache. On the next subsequent request, the app will retry revalidating the data.
Summary
In this tutorial you used useSWR
hook to fetch and paginate data. You learned how to auto-revalidate data when the tab is refocused or when offline. To further deepen your knowledge learn how to use react-query
or rtk-query
to fetch data in your app
Posted on September 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.