codelord.net

Code, Angular, iOS and more by Aviv Ben-Yosef

Replacing Angular's Deep Watches with the $doCheck Lifecycle Hook

| Comments

After digging into Angular’s components $onChanges lifecycle hook to show how you can replace a regular shallow $scope.$watch with it, let’s see what the latest Angular has to offer when it comes to deep watches.

Deep watches are what Angular does when you supply a third argument to $scope with the value of true:

1
$scope.$watch(() => this.foo, this.handleChange, true);

Note: I’m using ES6’s arrow functions for brevity, but obviously you don’t have to.

By using deep watches, we can ask Angular to keep track of changes happening inside an object, by mutating it, instead of only notifying us when the object’s reference has changed (which is the default behavior of $scope.$watch).

While it’s possible to use $onChanges for being notified about changes to references on bindings, there’s no easy way to do it for mutations without $scope.$watch.

The problem is that Angular is moving off $scope, and we would like to be able to use it only where it’s really a must, so what can we do?

First, think whether you really need to be mutating state. In a lot of scenarios, using immutable models makes a lot more sense, and using one-way data flow makes code easier to maintain.

But, in cases you have to keep using deep watches, there’s some more modern solution offered by Angular. The $doCheck lifecycle hook is a bit peculiar. It’s basically a hook that Angular calls on each turn of the digest cycle, for us to add our logic in it. That allows us to regularly check if anything has changed and requires our component’s attention.

What this enables us to do, in practice, is replace our deep watches. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
function FooCtrl() {
  var previousFoo = undefined;

  this.$onInit = () => { previousFoo = angular.copy(this.foo) };

  this.$doCheck = () => {
    if (!angular.equals(this.foo, previousFoo)) {
      this.handleFooChange();
      previousFoo = angular.copy(this.foo);
    }
  };
}

Now, this is a bit of a mouthful. As you can see, when we want to drop deep watches we actually have to do quite some work on our own: keep track of the previous value, make the comparison, and make sure we are holding a copy of the value (otherwise we’ll never see changes).

Angular is basically telling us that deep watches belong to the past, and that there’s a way to keep doing them without scopes, but you’ll need to work for it. Personally, I don’t love the extra typing, but I think that if you have to keep using deep watches for now and are upgrading your code to the latest 1.6 standards, you should probably be doing this.

Nowadays, a modern Angular app should be making very small use of scopes, and deep watches are a big red flag usually. This makes sense both from the standpoint of moving to a one-way data flow, and for keeping your options open when it comes to migrating to Angular 2+ down the road.

“Maintaining AngularJS feels like Cobol 🤷…”

You want to do AngularJS the right way.
Yet every blog post you see makes it look like your codebase is obsolete. Components? Lifecycle hooks? Controllers are dead?

It would be great to work on a modern codebase again, but who has weeks for a rewrite?
Well, you can get your app back in shape, without pushing back all your deadlines! Imagine, upgrading smoothly along your regular tasks, no longer deep in legacy.

Subscribe and get my free email course with steps for upgrading your AngularJS app to the latest 1.6 safely and without a rewrite.

Get the modernization email course!

Comments