Articles

How to scale data fetching with SWR

BY Francesco Filippini

I first discovered SWR when I joined BOOM as a junior developer.

Whenever I had built applications before, I limited myself to creating an effect that inserted data into the state of the single component. Later, after trying SWR, I realized that without this library, some problems were difficult to manage. For example, reusing the code to call an API endpoint resulted in many identical calls, with only two solutions within my reach: 

  • to create a global state in redux, which involved adding complexity to the code and studying the library

  • to insert the data in a global state of the “App” component (the parent component of all the others) and do prop drilling to get the data to all the child components that needed it.

SWR was the only way to keep the code from getting dirty and maintain a high degree of speed when loading data.

Additionally, I had been positively impressed when I noticed the features that revolve around the SWR library because implementing them would have been very difficult and long for a beginner (like I was). 

For example, it’s very convenient to just have to call the “mutate” method from one component to update data in another component that is difficult to reach. 

And so SWR was, for me, the solution that sped up my work and kept the code clean, without any do-it-yourself solutions.

Scaling Fetching Data to SWR

At BOOM, after a period of experimentation to find the best technology for data fetching, we decided that SWR was best-suited for our needs.

The acronym SWR stands for Stale-While-Revalidate: a pattern used in applications to speed up the loading and updating of the UI that requires data from an external REST resource.

SWR implies a caching strategy, whereby data received from the server stored in a local cache (stale), can be revalidated if necessary (revalidate), and updated in the same cache to be served in the future.

The browser needs to know when a resource has to be updated and to do this it uses the data contained in 'Cache-Control', a header of the HTTP response from the server.

​Inside this header there are several attributes, of which we will consider two:

  • "max-age" - which indicates the number of seconds that determine the obsolescence of the data; 

  • and "stale-while-revalidate" - which indicates the number of "additional" seconds for which the cached data can still be used but needs to be updated and replaced with new data in the cache.

​Let’s illustrate this with the example of a server returning this header in the HTTP response to a resource:

Cache-Control: max-age=10, stale-while-revalidate=119

Depending on how much time elapses before the browser asks whether the data needs updating or not, three different situations can come across:

  • If the elapsed time is less than the "max-age" (so before the 10th second) the data stored in the local cache is still valid, and it is not necessary to make a new call to receive the updated data.

  • If the elapsed time is greater than the "max-age" and less than the "stale-while-revalidate" (between seconds 10 and 119) the data stored in the local cache are still valid and can be used temporarily. The browser must ask the server for updated data in order to update the cache and display the new data as soon as possible

  • If the elapsed time is greater than the "stale-while-revalidate" (beyond the second 119), the data in the local cache must be discarded because it is no longer valid. The browser must make a new call in order to take the updated data, insert it in the cache and finally display it. 

If you need updated data and you accept a certain degree of obsolescence, this pattern fully meets your needs!

​React SWR library and advantages

Stale-While-Revalidate is implemented in the SWR library (https://swr.vercel.app/) and allows us to apply the pattern to React applications.

​The greatest advantage is the automated process that takes care of receiving and updating the data, without requesting any interaction with the cache.

​Other features offered by SWR include:

  • useSWR hook returns two values: data  and error-based on the status of the request

  • useSWR hook, which also provides mutation function, to force data reload. 

  • the data is updated "on focus": SWR understands when you switch from one browser tab to another or when the computer stops and automatically updates data to make it easier to find it when you go back to the tab.

  • data revalidation is performed automatically when the connection is lost, so there is no need to manage it on the code side; the library will do it for us.

  • deduplication: components accessing the same resource in the local cache make SWR perform only one call. Let's take an example:

import useSWR from 'swr';

const Header = () => {
  const { data } = useSWR("/api/shop")

  return ...
};

const HeroSection = () => {
  const { data } = useSWR("/api/shop")

  return ...
};

const Footer = () => {
  const { data } = useSWR("/api/shop")

  return ...
};

const App = () => {
  return (
    <>
      <Header />
      <HeroSection />
      <Footer />
    </>
  );
};

An application has to use the data of the resource "shop" in 3 different components, and each component will handle the call to this resource internally via SWR.

​At the network level, there’s the execution of a single call to the server.  The hook will return the same data to all 3 components without executing the call multiple times.

​This way, it is also possible to avoid the creation of a "global state" (using e.g. React.Context or Redux) to share information across multiple components: SWR will act as the global state and return the value to all components that need it.

​This functionality can be used to scale any frontend application and generate interesting results using just a few lines of code.

​Let's take another example to illustrate how this amazing technology works. An API method returns the user's data through a GET on the /api/user endpoint, and we need to:

  • display the user's data

  • give the possibility to the user to refresh

  • manage the loading state

  • manage errors.

A simple and frequently used solution is to create a state, and through an event (like a React effect or a user click) ask for the resource and save it in the state.

​A possible implementation that does not involve the use of SWR could be this one:

import useSWR from 'swr';
import {fetcher} from 'somewhere'

const Profile = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    try {
      const response = await fetcher("/api/user");
      setData(response.data);
      setError(null);
    } catch (e) {
      setError(e);
    }
  }, [])

  useEffect(()=>{
    fetchData();
  }, [fetchData])

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;

  return (
    <>
      <div>hello {data.name}!</div>
      <button onClick={fetchData}>refresh</button>
    </>
  );
};

As you can see, we need an initial effect to make the first call to get the data, and we also need two states to save the data and a possible error.

​Let us now try a solution using SWR:

import useSWR from 'swr';

const Profile = () => {
  const { data, error, mutate } = useSWR('/api/user', fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return (
    <>
      <div>hello {data.name}!</div>
      <button onClick={() => mutate()}>refresh</button>
    </>
  );
};

This solution is cleaner and more compact at first glance. We don't need effects and states to save progress because SWR takes care of the cache, possible errors, and the initial call for us.

Furthermore, with this last solution, we have the features mentioned above to improve the user experience.

​In summing up, with SWR it is easier to manage situations that request a lot of resources, where the cache plays a fundamental role in speeding up the application.

At the same time, however, SWR is a library on which our project will depend, and as such, it needs to be updated and maintained over time.

​Migrating to a solution using SWR is quite simple and removes complexity from the application by adding useful functionality for the end-user.

​Alternatively, there are interesting alternatives to SWR, including react-query and SWR supports GraphQL requests.


The SWR project page.

Visual transformation in a snap