Reputation: 16974
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 .then
s 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
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
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
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
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 :
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