Fetching, caching and revalidating server-side data with SWR

Emmanuel Fordjour Kumah - Sep 19 '23 - - Dev Community

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:

  1. Display the cached response to the client for a specified duration( in seconds)

  2. Revalidates the data (i.e. request fresh data) asynchronously while still displaying the stale data.

  3. Get the up-to-date data and display the response to the user.

swr react-hooks

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 and axios 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);
Enter fullscreen mode Exit fullscreen mode

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 and useState hooks from React

  • Fetch the data once using the useEffect in the parent component.

  • In the callback function of the useEffect(), call the fetch() 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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/>). Type import useSWR from "swr" at the top of the component.

  • Within the <App/> component, create a fetcher function. The fetcher 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 like fetch and axios 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())
Enter fullscreen mode Exit fullscreen mode
  • 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 the key to the fetcher function and then proceed to fetch the data.
      • option is an object of options to help with data fetching
  • On the left hand side of the expression , we use the array destructuring to extract the fetched data, error, and isLoading and mutate 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 the data 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;
Enter fullscreen mode Exit fullscreen mode

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 the fetcher function

  • error: error that is thrown by the fetcher if unable to fetch the data

  • isLoading: This helps you display a loading state if there's an ongoing request.

  • isValidating: If there is a request or revalidation loading

  • mutate: 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 and side effects with useState and useEffect 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 and SingleCharacter.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;
Enter fullscreen mode Exit fullscreen mode

In the code snippet we:

  • Import the useSWR from "swr" and useState 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 and fetcher 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/> component

  • Whenever 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>
...
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

SWR revalidate
(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 })
Enter fullscreen mode Exit fullscreen mode
  • { 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 or Memory cache, then the response was served from the cache.

Cached data

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>
Enter fullscreen mode Exit fullscreen mode

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') 

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .