It is not uncommon to have an Angular directive or component that needs to perform some work when its bounded inputs are changed.
And we all know watches are bad for performance, and that you should only use them when you really need them. But sometimes your code really needs them. What to do?
With Angular 1.5’s introduction of components, and the back-porting of lifecycle hooks from Angular 2, we have cleaner ways of achieving this.
Note: This post will use components, but lifecycle hooks are available in Angular’s directive as well. You can make use of this technique even if your team hasn’t moved to components yet, as long as you’re using Angular 1.5 or later.
Let’s look at a an example component. For brevity I’ll be using ES6’s arrow functions, but of course you don’t have to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
As you can see, this is a very basic component that wraps around some native D3 code to render a chart.
Whenever its input binding, dateSeries
, is changed the component re-renders the chart.
And it keeps track of those changes using $scope.$watch
.
Now, let’s make use of the $onChanges
lifecycle hook.
$onChanges
is called automatically by Angular whenever an input binding is changed by the component’s parent.
A couple of important details to notice: $onChanges
only works with one-way bindings (and @
bindings), which is what you should be using 99% of the time, and $onChanges
is only triggered when the parent component reassigns the value.
It will not be triggered if you reassign it inside the component itself.
So let us update our component to use one way bindings and $onChanges
instead of a watch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
That’s about it.
We no longer need to inject $scope
, which is always a good thing.
And we also removed a watch: Angular has its own watch on the binding anyway in order to sync it between components, and we’re taking advantage of it.
Detecting the Initialization Call
Sometimes when using $watch
we would like to treat the first time it is called differently, since $watch
triggers immediately after starting a watch.
The way we identify it would be to write code such as this:
1 2 3 4 5 |
|
As you can see, we’d check if newValue
is the same as oldValue
, which is Angular’s way of telling us it’s the initial run of the watcher.
With $onChanges
we have a clearer way of achieving this:
1 2 3 4 5 |
|
As you can see, the changes
object comes with a handy isFirstChange()
method.
Keeping Track of the Previous Value
Another useful capability of $watch
is that whenever it was triggered it would supply our listener with both the current value and the previous value.
This allows the code to compare them:
1 2 3 4 5 6 |
|
Fear not, the changes
object still got you covered:
1 2 3 4 5 6 7 8 |
|
Gotcha: $onChanges
and $onInit
It just so happens that Angular triggers the initial $onChanges
right before calling the $onInit
hook.
You should be aware of that when you write your component’s lifecycle hooks and make sure that you don’t rely in $onChanges
on anything that gets setup by $onInit
, and if so, make sure to account for it on the first change call.
That’s it!
You just got rid of some needless $scope.$watch
calls.
Better performance, and modern code – win!
Pat yourself on the back for me.
“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.