Chaining jQuery Ajax Calls

Starting with version 1.5, jQuery has provided Deferred Objects to make working with asynchronous functions a little easier. A canonical example of its usage might be:

$.getJSON('/dataz').then(function(objects) {
  alert('I got some json data: ' + objects.length);
});

I was recently working on some code that required making a number of sequential Ajax requests where the results of one request would determine the URL of a subsequent request. Additionally the results of later requests were building on the results of earlier requests.

By using Deferreds I was able to keep my code fairly straightforward, but there was still quite a bit of duplication and nested callback functions in .then() after .then(). What I wanted was to define a sequence of Ajax requests without having to deal with nested callbacks. I put together a chainedGetJSON function that would allow sequentially chaining AJAX calls in this manner.

In order to walk through this I have come up with a contrived example using the Big Huge Thesaurus API. This example will first retrieve some synonyms for a given word, followed by retrieving the synonyms for one of the words returned from the first query, and so on.

The first thing needed is a function that will create a URL for the Big Huge Thesaurus API given a list of words. In this example I am always looking up synonyms for the last word in the provided list.

var thesaurusUrlForLastWord = function(words) {
  var word = words[words.length - 1];
  return "http://words.bighugelabs.com/api/2/api-key/" 
         + word + "/json?callback=?";
};

With that defined I can now define the chainedGetJSON call to take a list of objects and some optional seed data. I think it will be easier to understand the function if you first see how it is used.

var promise = chainedGetJSON([
  { 
    url: thesaurusUrlForLastWord,
    process: function(json, words) {
      // Return all the synonyms (ignore the seeded word list)
      return json.noun.syn; 
    }
  },
  {
    url: thesaurusUrlForLastWord,
    process: function(json, words) {
      // Add new synonyms to the end
      return words.concat(json.noun.syn); 
    }
  },
  {
    url: thesaurusUrlForLastWord,
    process: function(json, words) {
      // Add the first three synonyms to the beginning,
      // just to do something different
      return json.noun.syn.slice(0,3).concat(words);
    }
  }
], ["food"]);

Each object provides a function to generate a URL given a list of words and a function to process the resulting JSON data from each request. The results of the first process function will be passed into the url and process functions of the second request, and so on.

The final parameter to the function, an array with only the word “food” in it, is seed data for the initial url function.

chainedGetJSON does not return the final results, but the promise of results to come. In order to get to the results you will need to provide a callback.

promise.then(function(words) {
  $('#output').text(words.join("\n"));
});

When the chain of Ajax calls is complete the list of words returned by the final process function will be passed to this callback which dumps the words into a div on the page.

And finally the definition of chainedGetJSON itself:

  
var chainedGetJSON = function(requests, seedData) {
  var seed = $.Deferred(),
      finalPromise;

  finalPromise = requests.reduce(function(promise, request) {
    return promise.pipe(function(input) {
      return $.getJSON(request.url(input)).pipe(function(json) {
        return request.process(json, input);
      });
    });
  }, seed.promise());

  // Start the chain with the provided seed data
  seed.resolve(seedData);

  return finalPromise;
};

I’ll break this down to explain what is being done. First a new Deferred object is created. This will provide a promise to boostrap the requests. When the resolve function of a Deferred is called it calls any registered done or then callbacks.

  var seed = $.Deferred(),
  ...
  seed.resolve(seedData);

The heart of chainedGetJSON is reducing the the list of request objects into a single promise (I would normally use the Underscore.js _.reduce function but I wanted to avoid dependencies other than jQuery for this example). The reduce call is initialized with a promise from the seed described above.

  finalPromise = requests.reduce(function(promise, request) {
    ...
  }, seed.promise());

Each iteration in the reduce is given a promise and a request definition. The pipe function is used to get a new promise whose values will be filtered by the provided function.

  return promise.pipe(function(input) {
    ...
  });

My filter function uses the provided url function (passing the given input) to get a URL for a $.getJSON call. $.getJSON returns a promise which I pipe into another filter function that calls the process function of the request.

  return $.getJSON(request.url(input)).pipe(function(json) {
    return request.process(json, input);
  });

The end result is a function that can be passed an array of request definitions to be executed in sequence, piping the results from one to the next.

You can see a working version of this code and play around with it here: http://jsfiddle.net/baconpat/35w46/.

Resources:
* The chainedGetJSON function can be downloaded here
* Deferred Objects documentation
* Fun With jQuery Deferred blog post

Conversation
  • JJ says:

    Interesting, thanks for sharing!

  • Comments are closed.