Angular Abyss: Understanding OnPush change detection
Dear NgReader, welcome to Angular Abyss series of articles made by me, which describe Angular’s most hot topics and common problems. Hope you enjoy!
All articles:
Problem:
We want to understand what is OnPush change detection strategy, how does it work with bindings and when to use it.
1. Setup
First of all, let’s create a project using Angular CLI.
ng new my-app --routing
I am using VS Code that has a nice command line shortcut to open editor in current directory.
cd my-app && code .
Now, use angular cli to run following commands.
ng g c components/parent
ng g c components/block-a
ng g c components/block-b
ng g c components/child-a
ng g c components/child-b
You should see such a structure:
Let’s build a simple components tree. We have a single Parent component that has two children: BlockA and BlockB. Blocks has their own children.
2. Build components hierarchy
Before we start, let’s add some css to easy visualize our nested blocks. Open app.component.css and paste this code:
Now open parent.component.ts and create an object called “data” that we will pass through the whole components hierarchy to every children and implement some methods to change this data object.
We created an data object with prop1 that represents a string and prop2 that is a number array.
Also we created three methods here, one to change a string property, another for push new value to the array and last method is creating a new object and assign it to data.
Remember that changeObject() not simply change the property, but creates a reference to completely new object. This is important for understanding change detection.
Go to parent.component.html and add a template code:
Second step is to create branches for BlockA — > ChildA and pretty the same for BlockB — > ChildB. I will show you code only for branch A, just to save your time 🕒. (see full project code link at the end)
Code for block-a.component.html:
And block-a.component.ts:
What is done here?
BlockAComponent receives data object through the Input() property. Renders the object in template with {{ data | json }} binding. It also logs event when bindings are changed (in our case when data property changes) and when a change detection change is performed for this component.
Do almost the same for BlockBComponent simply change the strings in template and console log.
Time to make changes to ChildAComponent. Open child-a.component.html:
And very similar code here:
Child component does almost the same job as block component. The difference only that block contains a child.
Do the same operations for ChildBComponent.
3. How One-way binding works with Objects
Now run the app and click the buttons. 🌟 See the console logs.
ng serve -o
What do we see? Even if we used a one-way binding syntax,
<app-child-a [data]="data"></app-child-a>
the object reference is shared.
It means that no matter which component do changes to data object’s properties it changes everywhere.
When you click “ChangeString” button and change data.prop1, you see in console log only “check” messages, not the “change”. Why?
It happens because Input() property binds the object reference which stays the same. Only object properties values are changed.
If you click “ChangeObject” button, the “change” logs appears. Now the ngOnChanges() method is called.
4. Okay, is it good or bad?
Sometimes we can use it with favor, but in most cases bugs with object properties changed somewhere in the components tree are hard to catch. 🐛
Moreover, change detection checks are performed from the top to bottom of a tree. This can lead to multiple checks with deeply nested hierarchies.
Solution?
5. OnPush change detection strategy
We can do a one line change to make the one branch behave different. All that we need to do is set ChangeDecectionStrategy to OnPush.
This is done in component data annotation before class.
What’s changed?
Now when you click a button “ChangeString” nothing change in A branch. BlockA and ChildA data objects didn't change.
But when you click “ChangeObject” button data is changed everywhere.
Summary: Basically when we bind objects and pass the through the Input() properties, components share the same object reference. Any changes to object properties will be reflected in every component.
When we change component change detection strategy to OnPush, we tell Angular to perform change detection only when Input() receives a new object reference. Same for simple types like string,number, boolean.
See the code here.
🚀 If you read something interesting from that article, please like and follow me for more posts. Thank you NgReader! 😏