Memoizing Angular filters to stop digest errors

tl;dr Use underscore's memoize method with your Angular filters to make them run faster and avoid nasty digest loops.

Here's the problem. You have an Angular filter that you're planning to use in an ng-repeat directive.

angular.module("TestApp.filters", []).filter("numbersToObjects", function(){
    //Takes an array of integers and returns an array of objects with numbers published under the 'val' property
    return function(numbers) {
       var numberObjects = [];
       for(var x = 0; x < numbers.length; x++){
          numberObjects.push({val: numbers[x] });
      }
      return numberObjects;
    }
})

Why do this? In my case, the reason was that the items in a collection you're planning to use ng-repeat on must all be unique. My array of integers had duplicate numbers in it. Turning the integers into objects would ensure uniqueness. Even if object[0].val and object[3].val were the same, the objects those properties attached to would still be unique.

<ul>
    <li ng-repeat="number in numbers | numbersToObjects">  </li>
</ul>

I thought I was being pretty clever, but unfortunately my solution caused another error. The thing is, the filter worked, but the error polluted the console log. It was the dreaded 10 $digest() iterations reached. Aborting! error. Why was it happening?

Here's what was going on. Each digest cycle, filters get executed, taking in some input and sending out some output. Angular knows it doesn't need to run another digest cycle if and only if the output of the filter on this digest cycle is identical to the output of the filter from the previous one (assuming the input is the same.) In other words, the digests repeat until things settle down.

Take this for example:

angular.module("TestApp", [])
  //GOOD FILTER: ELEPHANT === ELEPHANT
  .filter("capitalizeString", function(){
    return function(aString){
      return aString.toUpperCase();
    }
  })
  //BAD FILTER -- DO NOT DO THIS: 5.02 !== 5.05 (or whatever)
  .filter("addUniqueIdToNumber", function(){
    return function(numbers){
      console.log("Running filter");
      number += Math.random();
    }
  });

Send the string "elephant" into the first filter and it sends out "ELEPHANT" every time. Send the number 4 into the second filter and you get a digest loop because the output will always be a little different even if the input is the same.

It's worth thinking about what identity means in JavaScript when it comes to objects. Consider:

var object1 = {val: 5};
var object2 = {val: 5};
console.log(object1 === object2); //false.

Two objects can have identical properties, with identical values, and still not be identical. Now check this:

var object1 = {val: 5};
var object2 = object1;
object2.val = 10;
console.log(object1 === object2); //true
console.log(object1.val);          // 10

Although JavaScript doesn't have pointers, I think I understand pointers better after seeing examples like this one!

So the problem was that my filter would output different objects each time it was run, even if the input array never changed. [1, 2, 3] would yield [{val: 1}, {val: 2}, {val: 3}] every time the filter was run, but the output objects would be different every time (despite the properties being exactly the same.) Angular would detect this difference and repeat the digest, same as in my example with the unique ID filter.

The solution: Memoize the filter function

For identical input, a good function returns identical output. Unfortunately, if your function creates objects from its inputs, it will never return identical output for identical input.

Solution: underscore's memoize method. We basically want the filter function to keep a table of its outputs and inputs. If it gets an input it has seen before, we want it to return exactly the same output. Identical output for identical input will break the digest cycle. That's what _.memoize() does. Feed it a function, and it returns a version of that function that keeps track of inputs and outputs in the way I just described.

That way, the filter returns exactly the same objects it did the first time it was run, as long as the input to the filter was the same.

// The memoized filter function
angular.module("TestApp.filters", [])
  .filter("numbersToObjects", function(){
      //takes an array of integers and returns an array of objects with the numbers published under the specified property
      return _.memoize(function(numbers, propertyToPublishNumberUnder){
        propertyToPublishNumberUnder = propertyToPublishNumberUnder || "val";
        var objects = [], object;
        angular.forEach(numbers, function(number, index){
          object = {};
          object[propertyToPublishNumberUnder] = number
          objects.push(object);
        });
        return objects;  
      });
    //}
  }) 

Notes:

[1] If I remember right, filters run several times per digest cycle. That means they need to be light and not do any resource-intensive computations. However, if that is unavoidable, memoizing the computation-heavy function would be a good idea.

[2] I'm pretty sure JavaScript's notion of identity flagrantly flouts Leibniz's principle of the identity of indiscernables.