Keeping Angular "service" list data in sync with controllers

Published Sep 22, 2014Last updated Aug 14, 2017

Learning Angular has been one of the greatest productivity boosts for rapid application development in my career. However, some of the common strategies implemented can be improved in my opinion.

In AngularJS, there is a great deal of importance placed on separation of concerns. One of the most practiced patterns for holding application state is move this data into angular services or factories. Which one of the former to use is totally a personal preference in my opinion (I opt for factories most of the time).

The purpose of this post is aimed at services that hold collections that update from remote data calls.

Targeted AngularJS version at time of writing: 1.2.19

Too many times, I have run across angular controllers bringing in the $scope service just to $watch a service collection:

The bad:

app.controller('Ctrl1', function($scope, DataFactory) {
  // bind the controller property to the service collection
  this.items = DataFactory.items;
  
  // watch the collection for changes
  $scope.$watch(watchSource, function(current, previous){
    this.items = current;
  });
  
  function watchSource(){
    return DataFactory.items;
  }  
});

app.factory('DataFactory', function($q, $timeout) {
  var svc = {};
  svc.items = [];

  svc.getDataStream = function() {
    
    var fakeData = [
      { id: 1, name: 'name 1' },
      { id: 2, name: 'name 2' },
      { id: 4, name: 'name 4' }
    ];
    
    // using $q to fake async data grab
    return $q.when(fakeData)
      .then(function(data) {
        svc.items = data;
      });
  };

  return svc;
});

I know what you are thinking “why doesn’t this just work by default?” We are clearly updating the DataFactory.items.

svc.getDataStream = function() {
  return $q.when(fakeData)
    .then(function(data) {
      // here we are clearly reseting the data
      // to the response of the call.. Why doesn't
      // it just databind?
      svc.items = data;
    });
};

The reason that angular does not “watch” this value is because when it set’s up the implicit $watch from a view, it references the original array from the DataFactory. But when the data comes back through $http, it replaces the property with a different array reference. Thus Angular can’t watch the collection without adding nasty $watch functions in your controllers.

So why is the $watch a bad thing in the controller?

It adds to the cognitive load needed to understand what is going on in the controller. When we look at code that others (or ourselves 6 months from now) wrote, being able to easily understand the what without spending a lot of time parsing the how is very beneficial. Any bindings in your view cause implicit watches to be set. Also, all $watch functions are executed for every $digest which may occur many times in a “digest cycle” (angular kicks these off with most interactions). Adding yet another watch is a pattern that may get you into performance issues in the future.

Surely we can remove the dependency on $scope just to watch this collection. Let us look at a very simple example of this separation adapted from Todd Motto.

The better:

app.controller('Ctrl1', function(DataFactory) {
  // bind the controller property to the service collection
  this.items = DataFactory.items;

  // invoke the call to get data
  DataFactory
    .getDataStream()
    .then(function() {
      // update the controller collection property
      this.items = DataFactory.items;
    }.bind(this));
});

// sample "service" for getting data
app.factory('DataFactory', function($q, $timeout) {
  var svc = {};
  svc.items = [];

  svc.getDataStream = function() {
    
    var fakeData = [
      { id: 1, name: 'name 1' },
      { id: 2, name: 'name 2' },
      { id: 4, name: 'name 4' }
    ];
    
    // using $q to fake async data grab
    return $q.when(fakeData)
      .then(function(data) {
        svc.items = data;
      });
  };

  return svc;
});

Now I’m not hating on this format. Here after every call to update the service data source, both the source and controller reference get updated. It works for what it is intended for and it is fairly easy to understand what is going on. However, it does not really address controllers other than the one calling the service updated being notified.

How about this:

app.controller('Ctrl1', function(DataFactory) {
  // bind the controller property to the service collection
  this.items = DataFactory.items;
 
  // but wouldn't it be so much better
  // to just call it and let it work
  DataFactory.getDataStream();
});

Doesn’t this just read so much cleaner?

Enter angular.copy. What angular.copy does when given a new array and a source array is empty the source (by setting length to 0 i think..) and then repopulate the array with the new array items.

Our end result:

var app = angular.module('app', []);

app.controller('Ctrl1', function(DataFactory) {
  this.items = DataFactory.items;

  DataFactory.getDataStream();
});

app.controller('Ctrl2', function($timeout, DataFactory) {
  // when this eventually fires and gets *remote* data again
  // our other controller will automatically sync up
  // without the need for the $watch function
  $timeout(DataFactory.getDataStream, 2000);
});

app.factory('DataFactory', function($q) {
  var svc = {};
  svc.items = [];

  svc.getDataStream = function() {
    
    var fakeData = [
      { id: 1, name: 'name 1' },
      { id: 2, name: 'name 2' },
      { id: 4, name: 'name 4' }
    ];
    
    // using $q to fake async data grab
    return $q.when(fakeData)
      .then(function(data) {
        // this is the magic
        angular.copy(data, svc.items);
      });
  };

  return svc;
});

This article was originally posted on my blog.

Discover and read more posts from Justin Obney
get started
Enjoy this post?

Leave a like and comment for Justin