0

I'm using Angular 1.5.8. The views in my app require different combinations of the same 3 ajax requests. Some views require data from all three, others require data from two, or even one single endpoint.

I'm working on a function that will manage the retrieval of this data, requiring the app to only call each endpoint once. I want the ajax requests to be called as needed, but only when needed. Currently I've created a function which works, but seems like it could use improvement.

The following function is contained within the $rootScope. It uses the fetchData() function to cycle through the get requests as requested. When data is retrieved, it is stored in the global variable $rootScope.appData and then fetchData() is called again. When all data is retrieved the deferred promise is resolved and the data is returned to the controller.

$rootScope.appData = {};

$rootScope.loadAppData = function(fetch) {
  var deferred = $q.defer();

  function getUser() {
    $http
      .get('https://example.com/api/getUser')
      .success(function(result){
        $rootScope.appData.currentUser = result;
        fetchData();
      });
  }

  function getPricing() {
    $http
      .get('https://example.com/api/getPricing')
      .success(function(result) {
        $rootScope.appData.pricing = result;
        fetchData();
      });
  }

  function getBilling() {
     $http
       .get('https://example.com/api/getBilling')
       .success(function(result) {
         $rootScope.appData.billing = result;
         fetchData();
       });
  }

  function fetchData() {
    if (fetch.user && !$rootScope.appData.currentUser) {
      getUser();
    } else if (fetch.pricing && !$rootScope.appData.pricing) {
      getPricing();
    } else if (fetch.billing && !$rootScope.appData.billing) {
      getBilling();
    } else {
      deferred.resolve($rootScope.appData);
    }
  }

  if ($rootScope.appData.currentUser && $rootScope.appData.pricing &&$rootScope.appData.billing) {
    deferred.resolve($rootScope.appData);
  } else {
    fetchData();
  }

  return deferred.promise;
};

An object fetch is submitted as an attribute, this object shows which ajax requests to call. An example call to the $rootScope.loadAppData() where only user and pricing data would be requested would look like this:

$rootScope.loadAppData({user: true, pricing: true}).then(function(data){
   //execute view logic. 
});

I'm wondering:

  1. Should the chaining of these functions be done differently? Is the fetchData() function sufficient, or is this an odd way to execute this functionality?
  2. Is there a way to call all needed Ajax requests simultaneously, but wait for all required calls to complete before resolving the promise?
  3. Is it unusual to store data like this in the $rootScope?

I'm aware that this function is not currently handling errors properly. This is functionality I will add before using this snippet, but isn't relevant to my question.

kravse
  • 126
  • 1
  • 8

2 Answers2

2

Instead of using the .success method, use the .then method and return data to its success handler:

function getUserPromise() {
    var promise = $http
      .get('https://example.com/api/getUser')
      .then( function successHandler(result) {
          //return data for chaining
          return result.data;
      });
    return promise;
}

Use a service instead of $rootScope:

app.service("myService", function($q, $http) {

    this.loadAppData = function(fetchOptions) {

        //Create first promise
        var promise = $q.when({});

        //Chain from promise
        var p2 = promise.then(function(appData) {
            if (!fetchOptions.user) {
                return appData;
            } else {
                var derivedPromise = getUserPromise()
                  .then(function(user) {
                    appData.user = user;
                    //return data for chaining
                    return appData;
                });
                return derivedPromise;
            );
        });

        //chain from p2
        var p3 = p2.then(function(appData) {
            if (!fetchOptions.pricing) {
                return appData;
            } else {
                var derivedPromise = getPricingPromise()
                  .then(function(pricing) {
                    appData.pricing = pricing;
                    //return data for chaining
                    return appData;
                });
                return derivedPromise;
            );
        });
        //chain from p3
        var p4 = p3.then(function(appData) {
            if (!fetchOptions.billing) {
                return appData;
            } else {
                var derivedPromise = getBillingPromise()
                  .then(function(user) {
                    appData.billing = billing;
                    //return data for chaining
                    return appData;
                });
                return derivedPromise;
            );
        });

        //return final promise
        return p4;
    }
});

The above example creates a promise for an empty object. It then chains three operations. Each operations checks to see if a fetch is necessary. If needed a fetch is executed and the result is attached to the appData object; if no fetch is needed the appData object is passed to the next operation in the chain.

USAGE:

myService.loadAppData({user: true, pricing: true})
  .then(function(appData){
    //execute view logic. 
}).catch(functon rejectHandler(errorResponse) {
    console.log(errorResponse);
    throw errorResponse;
});

If any of the fetch operations fail, subsequent operations in the chain will be skipped and the final reject handler will be called.

Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs. -- AngularJS $q Service API Reference - Chaining Promises

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Thanks for this answer. After reading your answer / doing more research it's quite obvious to me that this logic should be contained within a service. I've made that change in my project now. Do you know of a way to perform all of the required calls in parallel rather than sequentially, and then return the result when whichever call takes longest has finished? – kravse Dec 06 '16 at 16:04
  • 1
    Use `$q.all` to do XHRs in parallel. The sequential method is useful when the next XHR depends on information from the previous XHR. An example would be calculating shipping costs after retrieving a users location. – georgeawg Dec 06 '16 at 18:13
0

Found a good way to answer question 2 in the original post. Using $q.all() allows the promises to execute simultaneously, resolving once they all complete, or failing as soon as one of them fails. I've added this logic into a service thanks to @georgeawg. Here's my re-write putting this code into a service, and running all calls at the same time:

  services.factory('appData', function($http, $q) {
    var appData = {};
    var coreData = {};

    appData.loadAppData = function(fetch) {
      var deferred = $q.defer();
      var getUser = $q.defer();
      var getPricing = $q.defer();
      var getBilling = $q.defer();

      if (!fetch.user || coreData.currentUser) {
        getUser.resolve();
      } else {
        $http
          .get('https://example.com/api/getUser')
          .success(function(result){
            coreData.currentUser = result;
            getUser.resolve();
          }).error(function(reason) {
            getUser.reject(reason);
          });
      }

      if (!fetch.billing || coreData.billing) {
        getBilling.resolve();
      } else {
         $http
           .get('https://example.com/api/getBilling')
           .success(function(result) {
             coreData.billing = result;
             getBilling.resolve();
           }).error(function(reason) {
             getBilling.reject(reason);
           });
      }

      if (!fetch.pricing || coreData.pricing) {
        getPricing.resolve();
      } else {
         $http
           .get('https://example.com/api/getPricing')
           .success(function(result) {
             coreData.pricing = result;
             getPricing.resolve();
           }).error(function(reason) {
             getPricing.reject(reason);
           });
      }

      $q.all([getPricing.promise, getUser.promise, getBilling.promise]).then(function(result) {
        deferred.resolve(coreData);
      }, function(reason){
        deferred.reject(reason);
      });

      return deferred.promise;
    };

    return appData;
  });
kravse
  • 126
  • 1
  • 8
  • 1
    Be aware that the `.success` and `.error` methods are deprecated and have been [removed from AngularJS 1.6](https://github.com/angular/angular.js/pull/15157). Also there is no need to manufacture a promise with `$q.defer` as the $http service already returns a promise. For more information, see [Is this a “Deferred Antipattern”?](http://stackoverflow.com/questions/30750207/is-this-a-deferred-antipattern) – georgeawg Dec 06 '16 at 18:27