Why You Should Be Storing Remote Data in a Cache (and Not in State)

Lessons from React at scale

Jason Ankers
Nov 28 · 4 min read
Photo: Jacek Smoter

A common pattern in React for handling remote data is to fetch inside useEffect and copy the result into component state. Something like this:

What if userData needs to be read from other components at different levels of nesting?

For example, we might need to read user permissions from completely independent areas of our app. Passing data through props can quickly get unfeasible.

One solution might be to reach for a global state store like context, or a state management library like MobX or Redux. These could all work, but they introduce an unnecessary layer of complexity.

Even if we have no requirement to reuse the data across other components, it’s still verbose and messy. We need to repeat this boilerplate in every component where we fetch data.

The useEffect needs to be written and we need to maintain the exact same states of the asynchronous process each time. In a more realistic scenario, our component would likely have some local UI state mixed in there as well.

Remote data is read-only. It doesn’t belong in the same location as our UI state.

We can solve these problems by:

  1. Abstracting away the boilerplate of the asynchronous process (loading, error, etc.).
  2. Storing our remote data in an in-memory cache.

The former can be solved with a custom Hook, but handling your own in-memory cache is hard and time-consuming.

Thankfully, two new libraries solve both of these problems out-of-the-box: swr and react-query. Both libraries provide custom React Hooks for managing remote data fetching.

Let’s see how using swr can clean up our example from before:

useSWR() takes a unique identifier (key) and fetcher function for resolving the data.

In the example above, fetchUser can be any asynchronous function. This means you can use these libraries alongside any data-fetching API or library you’re already using (Fetch, axios, Apollo, etc.).

The unique identifier is used for caching and deduping requests. If we want to access the same data from another component, we can call useSWR with the same key and swr will retrieve it from the cache.

Both components are referencing the same underlying request instance, we don’t need to worry about duplication!

By default, the result of a request becomes stale after the dedupingInterval (staleTime in react-query). This means that if component A makes a request, then component B makes a request with the same key within the time interval, the data is retrieved from the cache.

If the request is made after the interval, the data will be refetched and the cache revalidated. swr uses a default interval of two seconds and react-query uses a default of zero.

Both libraries employ a stale-while-revalidate caching strategy. This means that if a request is marked stale, the previously cached data will be returned and revalidation will occur.

When the new data arrives, the data is updated to reflect the new state, and all components referencing that request key will re-render. You can read more about stale-while-revalidate in this article.

Both libraries are tiny in size (< 4kb at the time of writing) and have excellent documentation. They support suspense, polling, scroll restoration, retry-on-failure, window focus revalidation, and plenty more.

Window focus revalidation is a great technique for providing an always up-to-date UI. This quote from the swr docs puts it nicely:

“Components will get a stream of data updates constantly and automatically. Thus, the UI will be always fast and reactive.”

Alongside these features, the biggest benefit I’ve found from using a caching solution is the simplicity it brings to my codebase.

The fetching and reading of remote data are still colocated within my components and I can trust that I’m always providing up-to-date and in-sync data across my component tree.

There are only small differences between these two libraries. Deciding which to use in your application will likely come down to personal preference. Assess the API’s of each and decide which fits your application best.

Before using either, it’s worth noting that remote data handling is still an area of high activity within the React community.

Given that it’s still unclear whether or not React will provide a caching solution in the future, I see no harm in using either of these libraries to fill the gap. With the advent of stable concurrent mode getting closer, it’s very likely that data-fetching solutions will continue to evolve.

Note: swr and react-query currently include ES6 features in their built output which means they will not work in IE11.

If you have the unfortunate responsibility of supporting IE11, you will need to transpile for ES5. In my case, I had to update my webpack config to include swr:

// ...

{
test: /\.(js|jsx|ts|tsx)$/,
include: [
resolve('src'),
resolve('node_modules/swr'),
],
use: ['babel-loader'],
},

Conclusion

Storing remote data in component state results in verbose and boilerplate-heavy components. Reaching for a global state store adds more complexity than it’s worth.

Using a data-fetching solution like swr or react-query will greatly simplify your code and improve your productivity.

I’m super excited about these libraries and hope that by sharing this excitement you find this technique useful in your own code, thanks for reading!

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade