Today, while working on my Angular 2 screencast series (announcement coming soon), I discovered a strange behavior when dealing with Angular 2 change detection. Here's what I found.

Contents are based on Angular version 2+.

Table of contents

Before starting, Angular 2 has implemented an awesome and very refined mechanism for detecting changes. As always, Thoughtram has an interesting article on their blog “Angular 2 Change Detection Explained” which goes deep into this topic and is definitely worth reading.

The Setup

We basically have two components, an app (as usual) and a child component <child> (I know…how brilliant :wink:). The parent component simply passes along a data object Person:

@Component({
	selector: 'app',
	template: `
		<child [person]="person"></child>
	`
})
class App {
	person: string;

	constructor() {
		this.person = 'Juri';
	}
}

So far so good. As you can see, in the App component’s constructor we initialize person to a string containing it’s name. In the child component we get that name as input and simply visualize it.


@Component({
	selector: 'child',
	template: `
		<h2>Child component</h2>
		{{ person }}
	`
})
class ChildComponent {
	@Input() person: string;
}

OnChanges: detect whenever @Input changes

Our goal is to teach our <child> component to understand whenever it’s @Input() property person changes. Angular 2 has an event for that: OnChanges.

In practice, this would look as follows:


import { OnChanges } from [email protected]/core';

@Component({
	selector: 'child',
	template: `
		<h2>Child component</h2>
		{{ person }}
	`
})
class ChildComponent implements OnChanges {
	@Input() person: string;

	ngOnChanges(changes: {[ propName: string]: SimpleChange}) {
		console.log('Change detected:', changes[person].currentValue);
	}

}

ngOnChanges(..) gets an object that contains every @Input property of our component as key and a SimpleChange object as according value.

Here’s a Plunker that demoes how this works. Open your dev console to see according logs being printed out.

So what’s the matter?

So far so good. Everything works as expected. The “issues” start when we change our person property from a native datatype into a JavaScript object.

@Component({
	selector: 'app',
	...
})
class App {
	person: any;

	constructor() {
		this.person = {
			name: 'Juri'
		};
	}

	// invoked by button click
	changePerson() {
		this.person.name = 'Thomas';
	}
}

Note that, in our changePerson() function, we now directly mutate the property of our person object which we pass on to our child component. All of a sudden, while the data binding still works, ngOnChanges is not being invoked any more. Check out the source here on this Plunker:

Instead, if we make our person object immutable, it works just fine:

@Component({...})
class App {
	...

	// invoked by button click
	changePerson() {
		this.person = {
			name: 'Thomas'
		};
	}
}

Try it out by yourself on the previous Plunker example! In fact, if we look up on the docs, even if a bit buried, we can find an according explanation:

Angular only calls the hook when the value of the input property changes. The value of the hero property is the reference to the hero object. Angular doesn’t care that the hero’s own name property changed. The hero object reference didn’t change so, from Angular’s perspective, there is no change to report!

But what if I always want to get notified?

So, cool, using immutable data structures definitely has some performance benefits on Angular 2 anyways. By using immutable data structures and fine tuning Angular 2 component’s change detection strategy, it can get insanely fast! Check out Jurgen Van de Moere’s article on How I optimized Minesweeper using Angular 2 and Immutable.js to make it insanely fast.

Ok nice, but I don’t care about immutable datastructures right now, how can I get to know about changes in my objects? DoCheck can be of help here. Let’s have a look.

What we can do is to implement the DoCheck lifecycle hook on our child component.


import { DoCheck } from [email protected]/core';

@Component({...})
class ChildComponent implements DoCheck {
	@Input() person: any;
	...
	ngDoCheck() {
		// called whenever Angular runs change detection
	}
}

ngDoCheck() is invoked whenever change detection is run

That allows us to implement some logic there, like remembering the old values and comparing them against new ones. There’s a smarter way, though, by using the KeyValueDiffers class.


import { DoCheck, KeyValueDiffers } from [email protected]/core';

@Component({...})
class ChildComponent implements DoCheck {
	@Input() person: any;
	differ: any;

	constructor(private differs: KeyValueDiffers) {
		this.differ = differs.find({}).create(null);
	}
	...
	ngDoCheck() {
		var changes = this.differ.diff(this.person);

		if(changes) {
			console.log('changes detected');
			changes.forEachChangedItem(r => console.log('changed ', r.currentValue));
			changes.forEachAddedItem(r => console.log('added ' + r.currentValue));
			changes.forEachRemovedItem(r => console.log('removed ' + r.currentValue));
		} else {
			console.log('nothing changed');
		}
	}
}

Slick, isn’t it :smiley:? Here’s a Plunker to play around with it:

Note, if you get a list as @Input, you can use IterableDiffers rather than KeyValueDiffers.

Conclusion

Nice, so we learned..


If you enjoyed this post you might want to follow me on Twitter for more news around JavaScript and Angular. :smiley: