Home

Cached fetch()

December 28, 2018

The fetch() is becoming de-facto standard from performing HTTP requests in browsers, being very widely used in place of the old XHR objects.

Sometimes though, when you’re fetch()ing data, you don’t want to actually hit the network each time this happens. It begs for a simple cache of results.

Since its API is very simple, we can easily wrap it in a bit of custom logic and conditionally fire off the actual request, depending of the cache state. In this particular case we’ll treat the URL of the request as the cache key. In a more complex scenario, we need to hash out the URL, headers and body of the request into a distinct and predictable key that can be used for caching the response.

Here’s the basic code that does it:

const store = new Map()

const cachedFetch = url =>
  store.get(url)
    ? new Promise(resolve => {
        resolve(store.get(url))
        store.set(url, store.get(url).clone())
      })
    : fetch(url).then(response => store.set(url, response.clone()))

The snippet is very simplistic but illustrates the basic mechanism of the caching.

A couple of notes:

  • For storage here we use a Map but it could easily be replaced with an empty object or any custom syncronous store that supports get()/set() interface.
  • We store a clone of the original response returned by fetch(), otherwise the body cannot be reused.
  • We don’t cover here network failures and we don’t invalidate cache in case they happen. This is crucial in production apps.

Usage:

const fetchData = async () => {
  const response = await cachedFetch(
    'https://baconipsum.com/api/?type=meat-and-filler'
  )

  return (await response.json())[0]
}
// Hits the network
fetchData()

// ...

// Doesn't actually hit the network, uses the response from the cache
fetchData()

Further improvements:

  • Use something more involved than URL in the cache key (Headers, HTTP Method, Request Body)
  • Perform proper network error checking and non-200 responses
  • API for forcing the network touch — useful when you need to refetch the response from scratch
  • Now we support only default options for cachedFetch() — start supporting the whole Fetch API.

Something similar is done in Remy Sharp’s memfetch. The only thing that I don’t like about it is that it overrides the global fetch() function. Basically this results in a global request interceptor which is forced upon the whole app code — sometimes this is not desired. Providing a local wrapper is a bit more transparent and elegant in my view.


Andrei Glingeanu

Andrei Glingeanu's notes and thoughts. You should follow him on Twitter, Instagram or contact via email. The stuff he loves to read can be found here on this site or on goodreads.