Angular 2 - View not updating after model changes
Asked Answered
M

5

164

I have a simple component which calls a REST api every few seconds and receives back some JSON data. I can see from my log statements and the network traffic that the JSON data being returned is changing, and my model is being updated, however, the view isn't changing.

My component looks like:

import {Component, OnInit} from 'angular2/core';
import {RecentDetectionService} from '../services/recentdetection.service';
import {RecentDetection} from '../model/recentdetection';
import {Observable} from 'rxjs/Rx';

@Component({
    selector: 'recent-detections',
    templateUrl: '/app/components/recentdetection.template.html',
    providers: [RecentDetectionService]
})



export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { this.recentDetections = recent;
             console.log(this.recentDetections[0].macAddress) });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

And my view looks like:

<div class="panel panel-default">
    <!-- Default panel contents -->
    <div class="panel-heading"><h3>Recently detected</h3></div>
    <div class="panel-body">
        <p>Recently detected devices</p>
    </div>

    <!-- Table -->
    <table class="table" style="table-layout: fixed;  word-wrap: break-word;">
        <thead>
            <tr>
                <th>Id</th>
                <th>Vendor</th>
                <th>Time</th>
                <th>Mac</th>
            </tr>
        </thead>
        <tbody  >
            <tr *ngFor="#detected of recentDetections">
                <td>{{detected.broadcastId}}</td>
                <td>{{detected.vendor}}</td>
                <td>{{detected.timeStamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
                <td>{{detected.macAddress}}</td>
            </tr>
        </tbody>
    </table>
</div>

I can see from the results of console.log(this.recentDetections[0].macAddress) that the recentDetections object is being updated, but the table in the view never changes unless I reload the page.

I'm struggling to see what I'm doing wrong here. Can anyone help?

Moy answered 28/4, 2016 at 15:40 Comment(1)
I recommend to make code more clean and less complex: https://mcmap.net/q/149464/-angular-2-view-not-updating-after-model-changesIgnorant
A
247

It might be that the code in your service somehow breaks out of Angular's zone. This breaks change detection. This should work:

import {Component, OnInit, NgZone} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private zone:NgZone, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                 this.zone.run(() => { // <== added
                     this.recentDetections = recent;
                     console.log(this.recentDetections[0].macAddress) 
                 });
        });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

For other ways to invoke change detection see Triggering change detection manually in Angular

Alternative ways to invoke change detection are

ChangeDetectorRef.detectChanges()

to immediately run change detection for the current component and its children

ChangeDetectorRef.markForCheck()

to include the current component the next time Angular runs change detection

ApplicationRef.tick()

to run change detection for the whole application

Abednego answered 28/4, 2016 at 15:43 Comment(21)
Another alternative is to inject ChangeDetectorRef and call cdRef.detectChanges() instead of zone.run(). This could be more efficient, since it will not run change detection over the entire component tree like zone.run() does.Lowestoft
@MarkRajcok I guess if you just change instance members this might be more efficient. What do you think about code that executes for example this.router.navigate() (with lots of side effects in other components) in code that runs outside Angulars zone. I think here detectChanges() might not fix the issue. What do you think?Weatherboard
Agree. Only use detectChanges() if any changes you make are local to the component and its descendants. My understanding is that detectChanges() will check the component and its descendants, but zone.run() and ApplicationRef.tick() will check the entire component tree.Lowestoft
This wasn't the issue (the real issue was a missing @Input annotation) but it's a good suggestion. I've seen quite a few questions on here where change detection was the issue.Moy
Missing input where? For what field?Weatherboard
The recentDetections field in the componentMoy
@John seems to be a very smart guy :) I can't see haw @Input() is related to your code even after having pointed it out. You get the value from a service. this.recentDetections = recent; how is @Input() related?Weatherboard
I have no idea. I'm hoping @John can explain it, because it definitely fixes the issue.Moy
Input is to pass values into the component like <recent-detections [recentDetections]="someValuesFromParent">Weatherboard
exactly what I was looking for.. using @Input() didn't make sense nor worked.Morion
@GünterZöchbauer can you explain why this is needed? What about this code forces us to explicitly call zone.run? Is it because the timer event doesn't force change detection?Jd
zone.js patches async APIs to get notified when they were called to run change detection afterwards. Usually when change detection isn't run (view is only updated when clicked on some button or similar) this indicates that some Angular code was called from a callback that is not covered by zone.js. I guess Observable.timer() is working fine now and this was some bug back then. Do you have this issue with Observable.timer() with recent versions of Angular2?Weatherboard
I'm still having this problem. We're using zone.run in every place we're updating a view from an observable. How do you use the async pipe with a zone.run, or is there an updated answer to this question?Promotive
Usually there is no need to use zone.run. With rxhs the issues should be fixed. Some APIs and 3rd party libraries are not fully supported but that should definitely be the exception. Can you reproduce your problem in a Plunker?Weatherboard
I really don't understand why we need all this manual stuff or why angular2 gods left such necessary. Why doesn't @Input mean that component really really is dependent on whatever the parent component said that depended on in its changing data? It seems extremely inelegant that it doesn't just work. What am I missing?Cleft
Worked for me. But this is another example why I have the feeling angular framework developers are lost in the complexity of the angular framework. When using angular as an application developer you have to spent so much time to understand how angular does this or does that and how to workaround things like questioned above. Terrrible!!Misdirection
getting error this.zone.run() is not a function , any idea ?Corduroys
Hi @PardeepJain What is this.zone? I'd need to see more code. Can you try to reproduce in stackblitz.com?Weatherboard
Unable to reproduce on stackblitz, but this.zone in my case is instance which i decalre in constructor, just same like your did in your example.Corduroys
Then it's probably not related to the code. There has to be some other issue. Hard to tell without a reproduction.Weatherboard
thanks your post help me fix my problem on cordova google maps plugin. when map was on focus the component stop to update values. using the NgZone that solve me the problem to bind realtime values....Vann
L
46

It is originally an answer in the comments from @Mark Rajcok, But I want to place it here as a tested and worked as a solution using ChangeDetectorRef , I see a good point here:

Another alternative is to inject ChangeDetectorRef and call cdRef.detectChanges() instead of zone.run(). This could be more efficient, since it will not run change detection over the entire component tree like zone.run() does. – Mark Rajcok

So code must be like:

import {Component, OnInit, ChangeDetectorRef} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private cdRef: ChangeDetectorRef, // <== added
                private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => {
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
} 

Edit: Using .detectChanges() inside subscibe could lead to issue Attempt to use a destroyed view: detectChanges

To solve it you need to unsubscribe before you destroy the component, so the full code will be like:

import {Component, OnInit, ChangeDetectorRef, OnDestroy} from 'angular2/core';

export class RecentDetectionComponent implements OnInit, OnDestroy {

    recentDetections: Array<RecentDetection>;
    private timerObserver: Subscription;

    constructor(private cdRef: ChangeDetectorRef, // <== added
                private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => {
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        this.timerObserver = timer.subscribe(() => this.getRecentDetections());
    }

    ngOnDestroy() {
        this.timerObserver.unsubscribe();
    }

}
Lagging answered 15/2, 2017 at 11:27 Comment(2)
this.cdRef.detectChanges(); fixed my problem. Thank You!Acheron
Thanks for your suggestion but after multiple attempts of call I get error: failure:Error: ViewDestroyedError: Attempt to use a destroyed view: detectChanges For me zone.run() helps me come out of this error.Dinghy
R
6

I know this is an old question but I wanted to share my situation. I work on a team and someone set the change detection strategy to be onPush basically disabling automatic change detection. Not sure if this helps anyway else but just in case

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

https://angular.io/api/core/ChangeDetectionStrategy

Roxieroxine answered 14/1, 2022 at 19:50 Comment(0)
T
2

In my case, I had a very similar problem. I was updating my view inside a function that was being called by a parent component, and in my parent component I forgot to use @ViewChild(NameOfMyChieldComponent). I lost at least 3 hours just for this stupid mistake. i.e: I didn't need to use any of those methods:

  • ChangeDetectorRef.detectChanges()
  • ChangeDetectorRef.markForCheck()
  • ApplicationRef.tick()
Twitty answered 5/2, 2018 at 18:32 Comment(2)
can you explain how you got your child component updated with @ViewChild? thanksFlighty
I suspect, parent was calling a method of a child class that wasn't rendered, and existed only in memoryIgnorant
I
1

Instead of dealing with zones and change detection — let AsyncPipe handle complexity. This will put observable subscription, unsubscription (to prevent memory leaks) and changes detection on Angular shoulders.

Change your class to make an observable, that will emit results of new requests:

export class RecentDetectionComponent implements OnInit {

    recentDetections$: Observable<Array<RecentDetection>>;

    constructor(private recentDetectionService: RecentDetectionService) {
    }

    ngOnInit() {
        this.recentDetections$ = Observable.interval(5000)
            .exhaustMap(() => this.recentDetectionService.getJsonFromApi())
            .do(recent => console.log(recent[0].macAddress));
    }
}

And update your view to use AsyncPipe:

<tr *ngFor="let detected of recentDetections$ | async">
    ...
</tr>

Want to add, that it's better to make a service with a method that will take interval argument, and:

  • create new requests (by using exhaustMap like in code above);
  • handle requests errors;
  • stop browser from making new requests while offline.
Ignorant answered 19/2, 2018 at 20:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.