JavaScript
Article

Mastering $watch in AngularJS

By Marcello La Rocca , Francisco Paulo

This article was peer reviewed by Mark Brown. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

AngularJS offers many different options to use the publish-subscribe pattern through three different “watch” methods. Each of them takes optional parameters that can modify its behavior.

The official documentation on $watch is anything but thorough: a problem that has afflicted AngularJS v1 as a whole, after all. Even online resources explaining how to proceed are, at best, scattered.

So, in the end, it becomes hard for developers choosing the right method for a given situation. And that’s especially true for AngularJS beginners! Results can be surprising or unpredictable, and this inevitably leads to bugs.

In this article I will assume some familiarity with AngularJS concepts. If you feel you need a refresher, you might want to read up on $scope, binding and $apply and $digest.

Check Your Understanding

For example, what’s the best way to watch the first element of an array? Suppose we have an array declared on our scope, $scope.letters = ['A','B','C'];

  • Will $scope.$watch('letters', function () {...}); fire its callback when we add an element to the array?
  • Will it when we change its first element?
  • What about $scope.$watch('letters[0]', function () {...});? Will it work the same, or better?
  • Above, arrays elements are primitive values: what if we replace the first element with the same value?
  • Now suppose the array holds objects instead: what happens?
  • What’s the difference between $watch, $watchCollection, and $watchGroup?

If you feel confused by all these questions, please keep reading. My aim is to make this as clear as possible through several examples, guiding you along the way.

$scope.$watch

Let’s start with $scope.$watch. This is the workhorse of all the watch functionality: every other method we will see is just a convenient shortcut for $watch.

$watch It

Now, what’s great about Angular is that you can use the same mechanism explicitly to perform complex actions in your controllers triggered by data changes. For instance, you could set a watcher on some data that can change in response to:

  1. Timeouts
  2. UI
  3. Complex asynchronous computations performed by web workers
  4. Ajax calls

You can just set up a single listener to handle any data change, no matter what caused it.

To do so, however, you need to call $scope.$watch yourself.

Hands On

Let’s take a look at the code for $rootscope.watch().

This is its signature: function(watchExp, listener, objectEquality, prettyPrintExpression).

In details, its four parameters:

  1. watchExp The expression being watched. It can be a function or a string, it is evaluated at every digest cycle.

    One key aspect to note here, is that if the expression is evaluated as a function, then that function needs to be idempotent. In other words, for the same set of inputs it should always return the same output. If this is not the case, Angular will assume that the data being watched has changed. In turn, this means that it will keep detecting a difference and call the listener at every iteration of the digest cycle.

  2. listener A callback, fired when the watch is first set, and then each time that during the digest cycle that a change for watchExp‘s value is detected. The initial call on setup is meant to store an initial value for the expression.

  3. objectEquality If, and only if, this is true the watcher will perform a deep comparison. Otherwise it performs a shallow comparison, i.e. only the references will be compared.

    Let’s take an array as an example: $scope.fruit = ["banana", "apple"];.

    objectEquality == false means that only a reassignment to the fruit field will yield a call to the listener.

    We also need to check “how deep” is a deep comparison: we’ll look at that later.

  4. prettyPrintExpression
    If passed, it overrides the watch expression. This parameter is NOT meant to be used in normal calls to $watch(); it is used internally by expression parser.

    Be careful: as you can see for yourself, it’s very easy to run into unexpected results when passing a 4th parameter by mistake.

Now we are ready to answer some of the questions in the introduction. Take a look at our examples for this section:

See the Pen Angular $watch demo – $scope.$watch() by SitePoint (@SitePoint) on CodePen.

Please feel free to familiarize with them; you can compare the difference in behavior directly, or follow the order in the article.

Watching an Array

So you need to watch an Array on your scope for changes, but what does “change” mean?

Assuming your controller looks something like this:

app.controller('watchDemoCtrl', ['$scope', function($scope){
    $scope.letters = ['A','B','C'];
}]);

one option is using a call like this one:

$scope.$watch('letters', function (newValue, oldValue, scope) {
    //Do anything with $scope.letters
});

In the callback above newValue and oldValue have self-explanatory meanings, and will be up to date each time it is called by the $digest cycle. The meaning of scope is intuitive too, as it holds a reference to current scope.

But, the point is: when will this listener be called? As a matter of fact, you can add, remove, replace elements in the letters array, and nothing will happen. This is because, by default, $watch assumes you only want referential equality, so only if you assign a new value to $scope.letters will the callback be fired.

If you need to act upon changes to any element of the array, you need to pass true as your third argument to watch (i.e. as the value of the optional objectEquality parameter described above).

$scope.$watch('letters', function (newValue, oldValue, scope) {
    //Do anything with $scope.letters
}, true);

Watching an Object

For objects, the deal doesn’t change: if objectEquality is false, you just watch for any reassignment to that scope variable, while if it’s true, every time an element in the object is changed the callback is fired.

Watching the First Element of an Array

It’s worth nothing that by watching an array with objectEquality === true, every time the callback is fired, newValue and oldValue will be the new and old values of the whole array. So you’ll have to diff them against each other to understand what actually changed.

Say, instead, you are interested in changes to the first element in the array (or the 4th – it’s the same principle). Well, since Angular is amazing, it lets you just do that: and you can express it in a natural way within the expression you pass as first argument to $watch:

$scope.$watch('letters[4]', function (newValue, oldValue, scope) {
    //...
}, true);

What if the array has only 2 elements? No problem, your callback won’t be fired until you add a 4th element. Well, OK, technically it will fire when you set up the watch, and then only when you add a fourth element.

If you log oldValue you’ll see that both times it will be undefined, in this case. Compare this with what happens if watch an existing element, instead: on the setup, you still have oldValue == undefined. So nothing that $watch can’t handle!

Now a more interesting question: do we need to pass objectEquality === true here?

Short answer: sorry, there is no short answer.

It really depends:

  • In this example, as we are dealing with primitive values, we don’t need a deep comparison, so we can omit objectEquality.
  • But suppose we had a matrix, say $scope.board = [[1, 2, 3], [4, 5, 6]];, and we want to watch the first row. Then we probably would like to be alerted when an assignment like $scope.board[0][1] = 7 changes it.

Watching a Field of an Object

Perhaps even more useful than watching an arbitrary element in an array, we can watch an arbitrary field in an object. But that’s not a surprise, right? Arrays in JavaScript are objects, after all.

  $scope.obj = {'a': 1, 'b': 2};
  $scope.$watch('obj["a"]', function (newValue, oldValue, scope) {
    // ...
  });  

How Deep Is Deep Comparison?

At this point we still need to clarify one last, but crucial, detail: what happens if we need to watch a complex, nested object where each field is a non-primitive value? Something like a tree or a graph, or just some JSON data.

Let’s check it out!

First, we need an object to be watched:

  $scope.obj = {
    'a': 1,
    'b': {
      'ba': {
        'bab': 2
      },
      'bb': [
        {
          'bb1a': 3,
          'bb1b': 4
        },
        {
          'bb2a': 5
        }
      ]
    }
  };

Let’s set our watch for the whole object: I assume that, by now, it is clear that objectEquality must be set to true in this case.

$scope.$watch('obj', function (newValue, oldValue, scope) {
    //...
}, true);

The question is: will Angular be kind enough to let us know when, say, an assignment like $scope.b.bb[1].bb2a = 7; happens?

And the answer is: yes, luckily for us, it will (check it out on the previous CodePen demo).

Other Methods

$scope.$watchGroup

Is $watchGroup() really a different method? The answer is no, it is not.

$watchGroup() is a convenient shortcut that allows you to set up many watchers with the same callback, passing an array of watchExpressions.

Each of the expressions passed will be watched using the standard $scope.$watch() method.

  $scope.$watchGroup(['obj.a', 'obj.b.bb[1]', 'letters[2]'], function(newValues, oldValues, scope) {
    //...
  });

It is worth noting that, with $watchGroup, newValues and oldValues will hold a list of the values for the expressions, both the ones that did change and the one that kept the same value, in the same order as they are passed in the first parameter’s array.

If you checked the documentation for this method, you might have noticed that it doesn’t take in an objectEquality option. That’s because it shallow watches the expressions, and only reacts to reference changes.

If you play around with the demo below for $watchGroup(), you might be surprised by some subtleties. For instance, unshift will cause the listener to be called, at least up to a certain point: that’s because when passing a list of expressions to $watchGroup, any of them firing will cause the callback to be executed.

See the Pen Angular $watch demo – $scope.$watchGroup by SitePoint (@SitePoint) on CodePen.

Also, note how no change to any of $scope.obj.b‘s’ subfields will produce any update – only assigning a new value to the b field itself will.

$scope.$watchCollection

This is another convenient shortcut to watch arrays or objects. For arrays, the listener will be called when any of the elements is replaced, deleted, or added. For objects, when any property is changed. Again, $watchCollection() doesn’t allow objectEquality, so it will only shallow watches elements/fields, and won’t react on changes to their subfields.

See the Pen Angular $watch demo – $scope.$watchCollection() by SitePoint (@SitePoint) on CodePen.

Conclusion

Hopefully these examples helped you to discover the power of this Angular feature, and understand how it is important to use the right options.

Feel free to fork the CodePens and experiment with the methods in different contexts, and don’t forget to leave your feedback in the comments area!

If you’d like to get a deeper understanding for some of the concepts we tackled in this article, here are a few suggestions for further reading:

  1. AngularJS scopes
  2. Understanding Angular’s $apply() and $digest()
  3. Emerging Patterns in JavaScript Event Handling
  4. Prototypal Inheritance in AngularJS Scopes.
  5. Documentation for $watch &co.
Marcello La Rocca
Meet the author
I'm a full stack engineer with a passion for Algorithms and Machine Learning, and a soft spot for Python and JavaScript. I love coding as much as learning, and I enjoy trying new languages and patterns.
Francisco Paulo
Meet the author
Francisco is a developer at Twitter, putting bits together into random shapes in the hopes that they'll somehow be useful. In his spare time he restores people to full health with his Tauren Shaman #WalkWithTheEarthMother.
  • http://mlarocca.github.io Marcello La Rocca

    Hi,
    just took a quick look, and I was wondering if, instead of going through $watch, it could be an easier solution if you used $rootScope.$emit and catch the events in chartDirective using $rootScope.$on?

    I’ll take a look at your example on jsfiddle, that seems to be working correctly though, as you mentioned. Please feel free to show more of your original (not working) code, that could help debugging.

  • mrbrockpeters

    Geez, it’s so tough to find a good and solid reference on $watch, even on the AngularJS documentation site itself . Thank you so much for this. This opens a more possibilities for me.

    Good Karma for you.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.