typescriptangular6observablerxjs6angular-changedetection

Angular 6 View is not updated after changing a variable within subscribe


Why is the view not being updated when a variable changes within a subscribe?

I have this code:

example.component.ts

testVariable: string;

ngOnInit() {
    this.testVariable = 'foo';

    this.someService.someObservable.subscribe(
        () => console.log('success'),
        (error) => console.log('error', error),
        () => {
            this.testVariable += '-bar';

            console.log('completed', this.testVariable);
            // prints: foo-Hello-bar
        }
    );

    this.testVariable += '-Hello';
}

example.component.html

{{testVariable}}

But the view displays: foo-Hello.

Why won't it display: foo-Hello-bar?

If I call ChangeDetectorRef.detectChanges() within the subscribe it will display the proper value, but why do I have to do this?

I shouldn't be calling this method from every subscribe, or, at all (angular should handle this). Is there a right way?

Did I miss something in the update from Angular/rxjs 5 to 6?

Right now I have Angular version 6.0.2 and rxjs 6.0.0. The same code works ok in Angular 5.2 and rxjs 5.5.10 without the need of calling detectChanges.


Solution

  • As far as I know, Angular is only updating the view, if you change data in the "Angular zone". The asynchronous call in your example does not qualify for this. But if you want, you can put it in the Angular zone or use rxjs or extract part of the code to a new component to solve this problem. I will explain all:

    EDIT: It seems like not all solutions are working anymore. For most users the first Solution "Angular Zone" does the job.

    1 Angular Zone

    The most common use of this service is to optimize performance when starting a work consisting of one or more asynchronous tasks that don't require UI updates or error handling to be handled by Angular. Such tasks can be kicked off via runOutsideAngular and if needed, these tasks can reenter the Angular zone via run. https://angular.io/api/core/NgZone

    The key part is the "run" function. You could inject NgZone and put your value update in the run callback of the NgZone object:

    constructor(private ngZone: NgZone ) { }
    testVariable: string;
    
    ngOnInit() {
       this.testVariable = 'foo';
    
       this.someService.someObservable.subscribe(
          () => console.log('success'),
          (error) => console.log('error', error),
          () => {
          this.ngZone.run( () => {
             this.testVariable += '-bar';
          });
          }
       );
    }
    

    According to this answer, it would cause the whole application to detect changes, whereas your ChangeDetectorRef.detectChanges approach would only detect changes in your component and it's descendants.

    2 RxJS

    Another way would be to use rxjs to update the view. When you first subscribe to a ReplaySubject, it will give you the latest value. A BehaviorSubject is basically the same, but allows you to define a default value (makes sense in your example, but does not necessary be the right choice all the time). After this initial emission is it basically a normal Replay Subject:

    this.testVariable = 'foo';
    testEmitter$ = new BehaviorSubject<string>(this.testVariable);
    
    
    ngOnInit() {
    
       this.someService.someObservable.subscribe(
          () => console.log('success'),
          (error) => console.log('error', error),
          () => {
             this.testVariable += '-bar';
             this.testEmitter.next(this.testVariable);
          }
       );
    }
    

    In your view, you could subscribe to the Subject using the async pipe:

    {{testEmitter$ | async}}
    

    3 Extract code to new Component

    If you submit the string to another component, it will also be updated. You would have to use the @Input() selector in the new component.

    So the new component has code like this:

    @Input() testVariable = '';
    

    And the testVariable is assigned in the HTML like before with curly brakets.

    In the parent HTML View you can then pass the variable of the parentelement to the child element:

    <app-child [testVariable]="testVariable"></app-child>
    

    This way you are in the Angular zone.

    4 Personal preference

    My personal preference is to use the rxjs or the component way. Using detectChanges oder NGZone feels more hacky to me.