hippietrail
hippietrail

Reputation: 16974

How to go about caching promises when working with the new fetch API?

Sometimes you want to use the same promised object in multiple times and places. Such as a promise for an AJAX resource from a URL.

It's difficult to cache the object because you don't know when it will asynchronously be avaible.

That's OK because you can call .then() multiple times on a single promise so you cache it the first time you need it and access the cache the subsequent times. And when it's available all the .thens will be carried out.

But when the promise is from the new fetch API it doesn't appear to be so straightforward... because a second promise is involved.

fetch() itself returns a promise and when it's fulfilled you must access the HTTP response's body via a second promise using a method such as .blob() or .json(). These are part of the "Body mixin".

Vanilla example of how to use fetch():

fetch(url).then(function(resp) {
  return resp.json().then(function(json) {
    // use the JSON
  });
});

The problems is that even though you can call .then() multiple times you can't call the functions such as .blob() and .json() multiple times on a single response.

It seems then that the result of .json() etc would also need to be cached, but that ought to be more difficult since it will only be available asynchronously. The resource could be requested twice before the two asynch promises resolve - a race condition.

Am I missing something? Is there an easy way to cache fetch's promise's JSON etc than I can't see? I can't seem to find this discussed on the net anywhere.

Upvotes: 2

Views: 7609

Answers (4)

Kurt Vangraefschepe
Kurt Vangraefschepe

Reputation: 130

Based on @Salva answer, I wrote a gist that uses lodash.memoize and response.clone().

import memoize from 'lodash.memoize';

const cache = memoize(req => {
    return fetch(req).then(response => {
        return response;
    });
}, req => req.url);

function cacheFetch(input, init) {
    return cache(new Request(input, init)).then(response => response.clone());
}

export default cacheFetch;

Upvotes: 0

hippietrail
hippietrail

Reputation: 16974

I have an updated and much more concise solution now that I understand promises a bit better. I didn't fully grok chaining via .then() after all and I realized my previous solution actually used "The Deferred anti-pattern" aka http://taoofcode.net/promise-anti-patterns/

let cache = {};

function cachingFetchJSON(url) {
  if (!cache.hasOwnProperty(url)) {
    console.log('fetching from afar');
    cache[url] = fetch(url).then(resp => resp.json());
  } else {
    console.log('fetching from cache');
  }
  return cache[url];
}

for (let i = 0; i < 5; ++i) {
  cachingFetchJSON("//jsonplaceholder.typicode.com/albums/" + (Math.floor(Math.random() * 4) + 1))
    .then(val => console.log(val))
    .catch(c => console.error('caught', c));
}

And here's the original solution I came up with after posting this question and learning from all the answers and comments:

Rather than just cache the promises returned as part of the fetch() API I create a new promise which is resolved only with the JSON text retrieved from the response body.

function fetchJSON(url) {
  return new Promise((resolve, reject) => {
    console.log('fetching from afar');
    fetch(url)
      .then(resp => resp.json()
        .then(json => resolve(json)))
      .catch(reason => reject(reason));
  });
}

let cache = {};

function cachingFetchJSON(url) {
  if (!cache.hasOwnProperty(url))
    cache[url] = fetchJSON(url);
  else
    console.log('fetching from cache');
  return cache[url];
}

for (let i = 0; i < 5; ++i) {
  cachingFetchJSON("//jsonplaceholder.typicode.com/albums/" + (Math.floor(Math.random() * 4) + 1))
    .then(val => console.log(val))
    .catch(c => console.error('caught', c));
}

Upvotes: 2

Salva
Salva

Reputation: 6847

You can not call .json() or .text() multiple times on a response because any of these methods consumes the body but you can copy the response before using it so you can simply cache the promise but instead of doing:

cachedFetch.then(response => response.json()).then(console.log);

Do:

cachedFetch.then(response => response.clone().json()).then(console.log);

Or wrap your fetch call inside a function that always returns a respone clone:

doFetch().then(response => response.json()).then(console.log);

Where:

var _cached;
function doFetch() {
  if (!_cached) {
    _cached = fetch('/something');
  }
  return _cached.then(r => r.clone());
}

Upvotes: 3

Walfrat
Walfrat

Reputation: 5353

I'm not familiar with es6 promise, however here is the basics how i do it in angularJS which is the same conceptually.

Instead of returning the promise given by fetch. You use a crafted one, this gives you the power to resolve it when you want :

  • Immediately if it's in the cache
  • When the server response is ready and parsed.

Here is how i handle caching in angluar's promise

var cache = [..]//fetching cache from somewhere using an API 
var deferred = $q.defer();//build an empty promise;
if(cache.contains('myKey')){
    var data = cache.get('myKey');
    // wrap data in promise
    deferred.resolve(data);
}else{
     fetch(req).then(function(resp) {
         resp.json().then(function(json) {
             cache.put('myKey', json);
             deferred.resolve(json);//resolve promise
         });
     });
 }
 return deferred.promise;//return the promise object on which you will be able to call `then`

Since es6 promise are really close of angular's one you should be able to adapt your code to fit.

Upvotes: 1

Related Questions