Change Detection: Getting in the (Angular) Zone!
Tags: angular, todayilearned
Published: 2020-01-24 23:11:29


Who knew an event binding in one component could cause a display bug in another? We will explain the issue before showing how using `NgZone` in the right place resolved it.

## Setting the Scene

We have a chart in our app to display data based on the user's selection. The work flow is as follows:

1. User makes a selection in a dropdown.
1. On closing the dropdown the selection is emitted.
1. An api call is triggered returning the data.
1. Chart updates to display the data.

However, following a change I made this week (I removed a CPU draining `setInterval`) the api call would return, but the chart would not update. Well not update until the user interacted with the page. Clearly this is a terrible user experience!

## Observable Firing but Template Not Updating

I could easily confirm that the updated data was arriving at the `ChartComponent` by `tap`'ing the observable pipe and logging the data.

```typescript
chartData$ = this.data.pipe(
    tap(data => console.log("Data updated", data)
);
```

So why wasn't the async pipe in my template updating the chart? And why does the data 'suddenly' appear when the user interacts with the page?

```html
<chart [data]="chartData$ | async"></chart>
```

Whenever you run into a situation like this you can be pretty sure you have a change detection issue. In this case Angular is failing to run a change detection cycle after the data has been updated. But why!?

## NgZones

If you are not familiar with [Zones in Angular](https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html) it will be worth reading that first. In summary asynchronous tasks can either run inside or outside of Angular's change detection zone. The delayed update suggests the event to update the chart is running outside of Angular's zone. However, our `ChartComponent` has no `ngZone` reference and usually you have to be explicit to run a task outside of Angular's zone?

## It's all about the *source event*

What took me sometime to discover was that I should not be looking at the end of the data pipeline but at the start. In particular at the event that kicks off the update.

Any event started outside of Angular's zone will run to completion outside without ever triggering change detection. No change detection, means no updates to our templates. This is sometimes [desired for performance](https://blog.thoughtram.io/angular/2017/02/21/using-zones-in-angular-for-better-performance.html) but we won't go into that here.

If we trace our chart update back through the api call, the NgRx Effect, the NgRx Action back to the dropdown Output event and finally to the eventEmitter inside the component I discovered the following code.

```typescript
@Component({...})
export class DropdownComponent implements OnInit {

    @Output()
    updateSelection = new EventEmitter<any>();

    ngOnInit(){
        $('#dropdown').on('hidden.bs.dropdown', () => {
            this.updateSelection.emit(this.selections);
        });
    }
}
```
## jQuery Event Handler

This code uses jQuery to watch for the [hidden event](https://getbootstrap.com/docs/4.0/components/dropdowns/#events) of a Bootstrap dropdown. This enables the component to fire an event when the dropdown is closed. The critical thing to note is that the Bootstrap `hidden.bs.dropdown` is fired outside of Angular's zone. Despite the fact we use an `@Output` `EventEmitter` this entire chain of events is run outside of Angular's zone.

This means that any side effects of this event will not be reflected in our template! This is exactly what we were seeing with out chart not updating. The data would 'suddenly' appear when some other event triggers a change detection cycle causing our chart to update at that point in time.

## Solving with NgZone

To fix this issue we need to make Angular aware of this event. We do this by wrapping the `EventEmitter` in the `ngZone.run()` method as follows.

```typescript
import { NgZone } from '@angular/core';

    constructor(private ngZone: NgZone) {}

    ngOnInit(){
        $('#dropdown').on('hidden.bs.dropdown', () => {
            this.ngZone.run(() => {
                // Bring event back inside Angular's zone
                this.updateSelection.emit(this.selections);
            });
        });
    }
}
```

This means the event is now tracked by Angular and when it completes change detection will be run! As we have applied this fix within our `DropdownComponent` all subsequent events forked off this originating one will also be checked. Important when using NgRx Actions and Effects!

## Fixing the ChartComponent the wrong way

My first approach to fixing this issue was to use `this.ngZone.run()` in my `ChartComponent`. While this fixes the chart, we would still be at risk of display inconsistencies!

For example, when an api call fails we display an error message to the user. With the fix only made in the `ChartComponent` this error message would not be displayed until the next change detection cycle. We could make the same fix in the `ErrorComponent` but now we are littering our code and who knows how many other times we will need to apply this fix.

In our case, it is important to bring the event back into Angular's zone as soon as possible. Otherwise every time this `DropdownComponent` is used we will have to repeat the fix.

## Why did I not notice this issue before?

This bug appeared when I remove a CPU intensive `setInterval` from another part of my app. It turns out, thanks to `zone.js`, `setInterval` fires events that are automatically within Angular's zone resulting in change detection. As the interval was set to `500ms` our chart would only ever be `500ms` out of date, hence why we did not notice this before. Not only have we fixed the underlying dropdown issue, we have a performance improvement too!

## Summary

Watch out for delayed template updates as they point to an event firing outside of Angular's zone. Secondly, don't rush to apply a fix before understanding the route cause, especially when it comes to change detection. As proof check out another time when the [quick fix](https://dev.to/scooperdev/when-using-settimeout-is-not-your-best-option-3p4b) was not my best option. 

### Note about HostListener

If I could have used a `HostListener`, as suggested by Isaac Mann and Wes Grimes on [Twitter](https://twitter.com/CooperDev/status/1220674670124523520), to capture the Bootstrap event then there would have been no need for `NgZone`. However, as explained in the thread I could not make this work.