4

I've read every imaginable article on JS promises, and I still can't figure them out. I'm attempting to use promises to manage multiple XMLHTTPRequests. eg: When xhr requests 1 & 2 complete, call this function.

Example code:

function initSession(){
  loadRates(0); 
  loadRates(10); 
  buildTable(); 
  // Get all rates from API, save to localStorage, then build the table. 
}


function loadRates(days) {
  var xhr = new XMLHttpRequest();
  xhr.onload = function() {
    // save response to localStorage, using a custom variable
    localStorage.setItem("rates" + days, xhr.responseText); 
  };
  xhr.open('GET', url);
  xhr.send();
}

function buildTable() {
  // get data from localStorage
  // write to HTML table
}

In this instance, how would I wrap each function call in a Promise object so that I can control when they are called?

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
ven88
  • 41
  • 2
  • 1
    Maybe [How do I promisify native XHR?](http://stackoverflow.com/q/30008114/1048572) helps – Bergi Jul 16 '15 at 21:44

2 Answers2

2

With callbacks, you always "respond" via one function. For example:

function getUsers (age, done) {
    // done has two parameters: err and result
    return User.find({age}, done)
}

Promises lets you respond according to the current state:

function getUsers (age) {
    return new Promise((resolve, reject) => {
        User.find({ age }, function (err, users) {
            return err ? reject(err) : resolve(users) 
        })
    })
}

This flattens the "callback pyramid." Instead of

getUsers(18, function (err, users) {
    if (err) {
        // handle error
    } else {
        // users available
    }
})

You can use:

getUsers(18).then((users) => {
    // `getPetsFromUserIds` returns a promise
    return getPetsFromUserIds(users.map(user => user._id))
}).then((pets) => {
    // pets here
}).catch((err) => {
    console.log(err) // handle error
})

So, to answer your question, first you'd want to use a promise for your http requests:

function GET (url) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()

        xhr.open('GET', url, true)
        xhr.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                return resolve(xhr.response)
            } else {
                return reject({ status: this.status, text: xhr.statusText })
            }
        }
        xhr.onerror = reject
        xhr.send()
    })
}

Then, you'd want to incorporate this into your loadRates function:

function loadRates (days) {
    var URL = URL_GENERATOR(days)
    return GET(URL).catch((err) => {
        // handle our error first
        console.log(err)
        // decide how you want to handle a lack of data
        return null
    }).then((res) => {
        localStorage.setItem('rates' + days, res)
        return res
    })
}

Then, in initSession:

function initSession () {
    Promise.all([ loadRates(0), loadRates(10) ]).then((results) => {
        // perhaps you don't want to store in local storage,
        // since you'll have access to the results right here
        let [ zero, ten ] = results
        return buildTable()
    })
}
royhowie
  • 11,075
  • 14
  • 50
  • 67
  • That first sentence… Shudder. Imo, you should think of promises as *return values* (that carry an asynchronous payload). – Bergi Jul 16 '15 at 21:30
  • @Bergi I guess promises are asynchronous operations that disregard how they're used (which is why they're generally nicer to work with than callbacks)…? – royhowie Jul 16 '15 at 21:31
  • I think it's pretty important to see that the *represent* the *result* of an (asynchronous) operation - neither they represent the operation itself, nor are they [just callbacks](http://stackoverflow.com/a/22562045/1048572) :-) – Bergi Jul 16 '15 at 21:37
  • @Bergi - I'd add one word to your summary: A promise represents the ***future*** result of an asynchronous operation. – jfriend00 Jul 16 '15 at 23:02
  • I don't quite understand what is meant by the first two sentences and corresponding code snippets. Wouldn't the first code snippet be simpler and more demonstrative if you actually showed the User.find function and the actual invokation of the callback? And I don't understand what you mean by "according to the current state" in the second sentence. Could you perhaps expand? – Hannes Johansson Jul 21 '15 at 07:58
  • Callbacks work with one function. If there's an error, you use `done(err)`; if everything worked, you use `done(null, object)`. Promises work by making you "pick a path." If there was an error, you either `throw` it or `Promise.reject`; otherwise, you `Promise.resolve`. By "current state," I meant whether or not there was an error—such determines whether you call `Promise.reject` or `Promise.resolve`. – royhowie Jul 22 '15 at 00:34
  • The reason I didn't elaborate on the `User.find` callback is because that (which was meant to be a mongoose query) is the convention in node: error first (callback) propagation; writing `User.find({}, function (err, done) { /* something */ })` is usually just redundant. – royhowie Jul 22 '15 at 00:40
  • @HannesJohansson tell me if that (somewhat) clarified things – royhowie Jul 22 '15 at 15:01
1

Also, be aware that there is convenient function to make an AJAX-requests and get promise – window.fetch. Chrome and Firefox already supports it, for other browsers it can be polyfilled.

Then you can solve your issue with ease

function loadRates(days) {
    return window.fetch(url).then(function(response) {
        return response.json(); // needed to parse raw response data
    }).then(function(response) {
        localStorage.setItem("rates" + days, response); 
        return response; // this return is mandatory, otherwise further `then` calls will get undefined as argument
    });
}

Promise.all([
   loadRates(0),
   loadRates(1)
]).then(function(rates) {
   return buildTable(rates)
})
just-boris
  • 9,468
  • 5
  • 48
  • 84