Part 1: Improving AngularJS Application with Immutable.js

angular-immutable
At Rescale, we’ve been venturing into the world of Facebook’s open source libraries with Immutable.js. Immutable.js is a library that provides immutable collections for javascript such as Set, Map, List, Stack, which are not available in ECMAScript 5. We found that these data structures improved the readability and sometimes the performance of our code (read on to see an example of this).
Another reason that we have decided to use Immutables was to eliminate the problems that arose due to the various ways of changing the UI “state”. Using Immutable.js you can replace your Plain Old Javascript Objects (POJO) with immutable maps, and every time something changes in your model your view will always have the correct reference to the latest status object.
To summarize, the benefits of using Immutable.js comes down to two points:

  1. Access to data structures such as Map, Set, List, Stack, etc
  2. Immutable design pattern that eliminates the problems associated with multiple states

In this article, I will show how OrderedSet can be used to simplify the code to illustrate the first benefit. I’ll start by introducing the original code, and before you read the code, here’s some context for the example.
We want to build a model for the UI that keeps track of selectedFiles across multiple pages, which are grabbed from the server as requested by the user. Because there could be thousands of files in total, the UI keeps only one page at a time, but stores all the files selected by a user across any number of pages. In this example, we specifically want to implement the “Select All / Deselect All” button, which selects/deselects the files that are currently shown.
Also note that the items could have already been selected from other pages, so you cannot simply add everything in displayFiles into selectedFiles, as that could potentially create duplicate copies in selectedFiles. Therefore, you need to compare the File objects using their ids instead of their references. To satisfy all the requirements, you end up writing something like this:

function FileManager() {
  this.displayFiles = [];
  this.selectedFiles = [];
}
// method that gets triggered when the user clicks “Select All / Deselect All” button
FileManager.prototype.toggleDisplayFiles = function(toggleValue) {
  var self = this;
  var existsInArray = function(arr, obj) {
    return _.some(arr, function(o) {
      return obj.id === o.id;
    });
  };
  // toggelValue is true -> user wants to select all files that are being displayed
  if (toggleValue) {
    _.each(self.displayFiles, function(df) {
      if (existsInArray(self.selectedFiles, df)) {
        self.displayFiles.push(df);
      }
    });
  } else {
    for (var i = self.selectedFiles.length - 1; i >= 0; i--) {
      var sf = self.selectedFiles[i];
      if (existsInArray(self.displayFiles, sf)) {
        self.selectedFiles.splice(i, 1);
      }
    }
  }
}

It may not be clear due to our liberal use of Underscore helper methods, but we’re essentially using a nested for loop to add or remove items in the selectedFiles array or the removing process, we’re iterating over the selectedFiles array from the end so that we can splice the array without affecting the iteration. Granted, it could be optimized with more helper functions and better bookkeeping, but it’s hard not to feel that we’re reinventing the wheel for implementing what is already known as a set.
With the use an OrderedSet, the code can be simplified to something like this:

function FileManager() {
  this.displayFiles = Immutable.OrderedSet();
  this.selectedFiles = Immutable.OrderedSet();
}
FileManager.prototype.toggleDisplayFiles = function(toggleValue) {
  if (toggleValue) {
    this.selectedFiles = this.selectedFiles.union(this.displayFiles);
  } else {
    this.selectedFiles = this.selectedFiles.subtract(this.displayFiles);
  }
};

It is definitely shorter, but there’s some magic to this approach that we haven’t fully disclosed yet. In the previous code, one of the crucial requirements for comparing files was to compare their ids instead of their references. In order to support this behavior with OrderedSets, we need to implement the hashCode method on the File class to define its uniqueness and the equals methods in a similar fashion that we do with classes that override equals and hashCode in Java. Here’s how this would look in our example:

File.prototype.hashCode = function() {
  var hash = 0, i, chr, len;
  if (id.length === 0) return hash;
  for (i = 0, len = this.id.length; i < len; i++) {
    chr = this.id.charCodeAt(i);
    hash = ((hash << 5) - hash) + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
};
File.prototype.equals = function(other) {
    return this.id === other.id;
};

Although, there is some extra code added to the class, the original method is much shorter and the readability of our code has been vastly improved. Considering that there are many more methods to be implemented involving File objects, the addition of hashCode and equals method on the File class will be well worth their lines.
Finally, in the controller we need to add the following watch statement since we have changed the type of selectedFiles into an instance of OrderedSet instead of a regular array.

var fm = new FileManager();
$scope.$watch(function() {
  return fm.selectedFiles;
}, function(newSF) {
  $scope.selectedFiles = newSF.toArray();
});

Now this might make you wonder whether there’s any performance issues with rendering because every time we update the selectedFiles, whether it is adding a single item or removing hundreds of items, we are updating the reference of $scope.selectedFiles to a new array. But since the File objects themselves are still the same objects containing Angular’s $$hashKey property, Angular compares the items in the new array using the $$hashKey and makes the minimum required DOM updates. In fact, there are some performance gains due to simpler $watch statements when we have a lot of watchers for a huge data structure which doesn’t change very often. But it is also important to remember that there is some overhead involved with creating new data structures every time something changes in the model*.
There are situations in front-end development when you want those data structures(e.g. Map, Set, Stack, Queue etc) found in the java Collections API, which are not available in ECMAScript 5. In those situations, using Immutable.js can help you improve not only the readability of your code, but also the performance of your application. As always, it is not a panacea that works in all cases, but more often than not, it will prove to be beneficial.
*If you’re interested in learning more about the trade-off between the benefits gained with simple watch statements and costs incurred by creating new data structures, check out this article.

Similar Posts