Angular 2 - View not updating after model changes

Angular

Angular Problem Overview


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?

Angular Solutions


Solution 1 - Angular

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 https://stackoverflow.com/questions/34827334/triggering-angular2-change-detection-manually

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

Solution 2 - Angular

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();
	}

}

Solution 3 - Angular

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

Solution 4 - Angular

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()

Solution 5 - Angular

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt WatsonView Question on Stackoverflow
Solution 1 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - AngularAl-MothafarView Answer on Stackoverflow
Solution 3 - AngularRobertView Answer on Stackoverflow
Solution 4 - AngularEleEVeryZeView Answer on Stackoverflow
Solution 5 - AngularglukkiView Answer on Stackoverflow