Mastering $watch in AngularJS

Share this article

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.

Frequently Asked Questions (FAQs) about Mastering $watch in AngularJS

What is the main purpose of $watch in AngularJS?

The $watch function in AngularJS is primarily used for observing changes in the value of a variable or an expression. It is a part of the AngularJS scope object and is used to monitor the changes in the value of a variable or expression. When a change is detected, the $watch function triggers a callback function, which is executed every time the watched variable changes.

How does $watch work in AngularJS?

The $watch function in AngularJS works by comparing the old and new values of the watched variable or expression. It uses the JavaScript’s equality operator (===) for comparison. If the new value is different from the old value, the $watch function triggers the callback function.

How can I use $watch in AngularJS?

To use $watch in AngularJS, you need to call the $watch method on the scope object and pass two arguments to it: the name of the variable or expression to watch, and the callback function to execute when the watched variable changes. Here is an example:

$scope.$watch('variableToWatch', function(newValue, oldValue) {
// code to execute when 'variableToWatch' changes
});

What is the difference between $watch and $apply in AngularJS?

The $watch function in AngularJS is used to observe changes in a variable or expression, while the $apply function is used to manually start the AngularJS digest cycle, which checks for any changes in the watched variables and updates the view accordingly. The $apply function is typically used when changes to the model are made outside of the AngularJS context, such as in a DOM event handler or a setTimeout function.

Can I watch multiple variables with $watch in AngularJS?

Yes, you can watch multiple variables with $watch in AngularJS. You can do this by passing an array of variable names to the $watch function. However, keep in mind that watching multiple variables can have a performance impact, as the $watch function needs to check for changes in all watched variables during each digest cycle.

How can I stop watching a variable with $watch in AngularJS?

When you call the $watch function in AngularJS, it returns a deregistration function. You can call this function to stop watching the variable. Here is an example:

var deregister = $scope.$watch('variableToWatch', function(newValue, oldValue) {
// code to execute when 'variableToWatch' changes
});

// later...
deregister(); // stops watching 'variableToWatch'

What is $watchGroup in AngularJS?

The $watchGroup function in AngularJS is used to watch a group of expressions. It works similarly to the $watch function, but it triggers the callback function only once per digest cycle, even if multiple watched expressions have changed. This can improve performance when watching multiple expressions.

What is $watchCollection in AngularJS?

The $watchCollection function in AngularJS is used to watch the properties of an object or the elements of an array. It triggers the callback function whenever any property or element changes, but unlike $watch, it does not deep-watch the object or array, which can improve performance.

Can I use $watch in AngularJS directives?

Yes, you can use $watch in AngularJS directives. In fact, it is a common practice to use $watch in directives to react to changes in the directive’s attributes or scope variables.

What are the performance considerations when using $watch in AngularJS?

Using $watch in AngularJS can have a performance impact, especially when watching many variables or expressions. This is because the $watch function needs to check for changes in all watched variables during each digest cycle. To improve performance, consider using $watchGroup or $watchCollection when appropriate, or limiting the number of watched variables.

Marcello La RoccaMarcello La Rocca
View 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 PauloFrancisco Paulo
View 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.

angularangularjsLearn Angularnilsonjwatches
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week