Confused about ChangeDetectorRef.detectChanges()

I have an app with two pages. One of them (HardwareIntegrationSensorsPage) subscribes to an observable (which happens to be BatteryStatus.onChange()).

That subscriber updates a property on the component, and then calls ChangeDetectorRef.detectChanges() so the view will actually be updated.

This works fine, as long as HardwareIntegrationSensorsPage is the very first page loaded by the app. If I navigate to OtherPage and come back – or if I tweak my app so that OtherPage loads first – then my call to .detectChanges() throws an error:

ViewDestroyedError: Attempt to use a destroyed view: detectChanges

This confuses me, because the constructor for HardwareIntegrationSensorsPage runs each time the page is entered. Which (I would think) means it should get a fresh ChangeDetectorRef.

Relevant code:

import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { NavParams } from 'ionic-angular';
import { Subscription } from 'rxjs/Subscription';

import { BatteryStatus, BatteryStatusResponse } from '@ionic-native/battery-status';

import { Slide } from '../../models/slide';

@Component({
    selector: 'page-hardware-integration-sensors',
    templateUrl: 'hardware-integration-sensors.html'
})
export class HardwareIntegrationSensorsPage implements OnInit, OnDestroy {

    private slide: Slide;

    private batteryStatus_subscription: Subscription;
    private batteryStatus_response: BatteryStatusResponse = null;

    constructor(
        private cd: ChangeDetectorRef,
        public navParams: NavParams,
        private batteryStatus: BatteryStatus,
    ) {
        this.slide = this.navParams.get('slide');
    }

    ngOnInit(): void {

        console.log('Subscribing to batteryStatus');
        this.batteryStatus_subscription = this.batteryStatus.onChange().subscribe((response: BatteryStatusResponse) => {
            this.batteryStatus_response = response;
            this.cd.detectChanges(); // this line is causing the error
        });

    }

    ngOnDestroy(): void {
        console.log('Unsubscribing from batteryStatus');
        this.batteryStatus_subscription.unsubscribe();

    }

}

Any hints? I’m still pretty hazy on exactly how manual change detection is supposed to work. I suspect that .detach() and/or .reattach() may be helpful here, but I don’t understand how I’m supposed to use them.

No it doesn’t. It only runs when the page is initially constructed.

Well, I put a console.log() in my constructor, and it did run each time I entered the page. (I’m using the sidemenu starter app, so navigation actions trigger a call to setRoot(), so there is no navigation stack. Or, more precisely, there’s a navigation stack, but it always contains exactly one item.)

At any rate: is there a standard approach to ensure that my reference to ChangeDetectorRef is current and valid?

It’s only a coincidence that entering coincides with construction. They are two separate life cycle events. Somewhere your code is expecting page teardown to be instantaneous. It isn’t. You probably have a process running that is not fully destroyed. Safest to assume that page teardown takes a long time. Anything you need destroyed before you leave a page you should do in a life cycle hook like ionViewWillLeave.

Are you the person who asked about a subscribe? If so, what’s almost certainly happening is your subscription is still alive. Unsubscribe from all Observables inside ionViewWillLeave.

Yep, that was me.

I was already unsubscribing – see code above (although I was doing it in ngOnDestroy(), rather than ionViewWillLeave()).

I just moved the unsubscribe to ionViewWillLeave(), and also moved the subscribe from ngOnInit() to ionViewWillEnter(). No change in outcome.

Anyway, I don’t think the subscription is the issue. The issue still happens if:

  • I set up my app to boot to OtherPage (which doesn’t subscribe to anything);
  • Then, I navigate to HardwareIntegrationSensorsPage (so, this page is loading for the first time).

Should I be setting changeDetection: ChangeDetectionStrategy.OnPush on my page component? I thought that was only for components that use @Input() or @Output(), but maybe I’m mistaken. (Edit: I just tried this, with no change in outcome.)

This is an advanced feature, and if you enable it, your page will not work the way standard documentation says pages work. I have this enabled in my projects for rendering performance reasons, but I had to learn to code with it, and there isn’t a lot of doc available. So I don’t recommend you mess with it for a while.

detectChanges refers to an html element. That element no longer exists when the function is called. The bad, neanderthal fix for this is to use applicationRef.tick() instead. That ticks literally everything in your application, so it’s bad for performance, but it isn’t view dependent. You might go that route to get something working, but it would be a bad idea for a production-ready app.

It’s hard to understand your problem without seeing more code. But the error code is clear. You’re calling detectChanges after a view has been destroyed.

I am a damn idiot.

I had some leftover code in my HomePage component which was, in fact, subscribing to the same Observable (and was also calling .detectChanges()). I just stripped out all of that code, and everything works perfectly now.

Thanks for all of your help – sorry to waste your time on something so dumb. I’ll try to pay it forward once I know what the hell I’m doing :slight_smile:

I’ve done the same damn thing, believe me. Honestly, I think this stuff is really hard.

1 Like