Change Detection in Non-Angular Callbacks

June 9th 2017 Angular Ionic 2/3

Recently, I was troubleshooting a curious bug that only happened on one page in my Ionic 2 application: new values from an HTTP request only showed on the page after a click instead of immediately. Wrapping the update code in NgZone.run() helped. However, it bothered me why this was only necessary in this single instance.

To get to the bottom of it, I first needed to read up on zones. Long story short, Angular uses zones (its own NgZone in particular) to detect changes that happen in asynchronous callbacks, so that it can update the bindings. If the callback somehow breaks out of NgZone, Angular's change detection isn't triggered and the views don't update. Obviously, this was somehow happening in my application on the page that failed to update immediately.

However, on first sight I wasn't doing anything different there: I was updating the data in response to Http.get() just like on many other pages. Finally, it occurred to me that maybe code wasn't breaking out of the zone at this very call, but already somewhere earlier in the asynchronous call stack. I starting tracing back through the calls in search of the root cause:

  • Http.get() was called from the constructor of the page.

      constructor(http: Http) {
        this.http.get(url).toPromise(response => {
          this.model = response.json();
        });
      }
    
  • The page was navigated to from a button on an Ionic popover page, which first dismissed the popover and then pushed the new page onto the navigation stack once that action resolved:

      this.viewCtrl.dismiss().then(() => {
        this.app.getRootNav().push(page, params);
      });
    
  • The popover was opened in response to the user clicking on a GoogleMap marker:

      let marker = new google.maps.Marker(markerOptions);
      marker.addListener('click', () => {
        let popover = this.popoverCtrl.create(page, params);
        popover.present();
      });
    

In the end, it turned out that the event handler was breaking out of the zone. I wrapped the event handler code in a NgZone.run() call and the problem was fixed:

    let marker = new google.maps.Marker(markerOptions);
    marker.addListener('click', () => {
      ngZone.run(() => {
        let popover = this.popoverCtrl.create(page, params);
        popover.present();
      });
    });

Unlike the original fix at the spot where I noticed the problem, this approach ensures that the issue will not turn up again somewhere else down the stack from the originating event handler.

In general, asynchronous code could break out of NgZone only when dealing with non-Angular code. This typically means third party libraries with no direct support for Angular. Till now I have also noticed the same behavior with IBM MobileFirst Foundation client-side API. The same fix solved that issue as well.

Copyright
Creative Commons License