Juri Strumpflohner

RSS

Fine grained change detection with Angular

Author profile pic
Juri Strumpflohner
Published

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

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

Tuning Angular's Change Detection

Videos and runnable code examples that demonstrate different tuning techniques for Angular's change detection.

The Setup

We basically have two components, an app (as usual) and a child component <child> (I know…how brilliant 😉). 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 has an event for that: OnChanges.

In practice, this would look as follows:


import { OnChanges } from '@angular/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 anyways. By using immutable data structures and fine tuning Angular 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 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 '@angular/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 '@angular/core';

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

	constructor(private differs: KeyValueDiffers) {
		this.differ = differs.find({}).create();
	}
	...
	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..

  • that ngOnChanges won’t be triggered when we mutate a (non-immutable) JavaScript object. Instead it triggers only when we reference-change the data-bound input property.
  • Also, we’ve seen that we can use ngDoCheck for such scenarios, that allows us to do a very fine-grained check of which property on our object changed.