Exploring Change Detection in Angular
On an afternoon while coding on an Angular application, I ran into the unfortunate situation of using a function call on *ngIf. The code worked of course and I was beaming until I opened the console and saw it getting bombarded with logs(I had placed a few inside the function). And it kept growing every time I did something on the app. It almost felt like my application was stuck in an infinite loop somewhere. I tried to google this out and came across many articles that said that placing a function inside the template is not a good practice. I was not convinced and wanted to know why so I started digging deeper. And then I came to know about the (in)famous Change Detection Strategy in Angular. So here is a short article on exactly that.
Change Detection in Angular
Change Detection basically means what to re-render in Angular and when. Every time an event(synchronous or asynchronous) takes place on your application, the Angular Change detection runs and re-renders components. Angular uses a method called refreshView() to re-render the views. What components have to be re-rendered is determined by the strategy that the change detection process is using. There are two kinds of strategies:
CheckAlways(default): refreshView() is called on the root components to all the child and subsequent components.
OnPush: refreshView() is called only on components with dirty views.
NgZone
Angular provides a service called NgZone to help with the change detection. In brief, a zone essentially enables you to create a new execution context that persists across asynchronous tasks. To read more about zones and why they're used you can refer to this article. Angular uses the zone to monkey patch async APIs(addEventListener, setTimeout(), ...) and uses notifications from these patched APIs to run change detection every time any async event happens.
All the code inside an angular application is executed within a zone called angular. This zone automatically triggers change detection when the following conditions are met:
When a sync or async function is executed
When there is no microtask scheduled in the Microtasks queue
When a zone detects a change it updates the components from top to bottom using the tick() method.
There are two zones that come into picture: The first one is this angular zone which facilitates change detection. It is also possible for a user to run code outside this zone (if you don't want the change detection to run on its own), in the other zone which is called the outer zone or parent zone. This outer zone has no change detection in place and user has to manually trigger a change detection run to update the view. This can be done by calling the runOutsideAngular() method provided by the NgZone service. Similarly, you can also call the run() method to put an async operation inside the angular zone. This scenario does not really ever comes up as Zone natively supports most of the common async operations. However there might be some third-party APIs that are not supported for which you can use to this method to configure automatic change detection.
Coming back to my problem...
So what did I do wrong in my code and how it could have been avoided or mitigated? First of all, after reading about change detection mechanism I too do not recommend calling a method from your template as you already read, it makes the view dirty and causes the change detection to run and re-render. This could lead to a lot of re-renders and potentially hamper the performance of your application. However, if it's really needed and you simply cannot avoid calling a function on ngIf you can make use of the OnPush strategy to help you reduce the number of change detection runs. At the same time you need to be careful while using this strategy otherwise it can lead to incorrect views. For example, if you have a component defined with this strategy then this component shall re-render when any input to this component changes. Let's say you have an object with many nested object properties as one of the inputs to this component. In your parent component, you change one of these nested properties and pass this updated object via input binding to the child component. The inputs of type object are compared by reference or primitive type. This means if an input is bound to an object and only a property of the object changes, no input change is detected. Therefore, in this situation the object in your child component did not really change and hence won't re-render which is of course wrong. To avoid such situations you can explicitly call detectChanges() from the change detector ref to trigger a change detection.
Hope this helped. Thanks and see you in the next one! <3
Subscribe to my newsletter
Read articles from Shweta Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by