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

7

93

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.

Exquisite answered 24/5, 2018 at 22:45 Comment(2)
Hey Danny, did my answer help you or do you need more information? If it helped, please mark it as accepted answer.Koontz
Yes, thanks. Didn't know about the concept of Zones.Exquisite
K
128

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.

Koontz answered 4/7, 2018 at 8:52 Comment(7)
Thanks, in my case the this.ngZone.run only workedDanielledaniels
Interesting. Angular seems to change the behaviour a little bit form version to version. It can be quite a headache sometimes. At least you found your solution. I am happy it helped.Koontz
I also tried the RxJS example you provided and it doesn't work like @GeorgeC. wrote, I suggest that you update your answer that this may work only on some versions of behaviour, but not with the latest.Spectacle
@Spectacle if you want to update an array you can try the spead operator: for more info look here developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… else try to play wit this code this.ngZone.run( () => { this.testVariable += '-bar'; });Danielledaniels
ngZone.run worked in my case while rxjs unfortunately didn't.Curren
great I did with Rxjs in latest version!!Abortionist
Signal could be added as wellAltercate
B
12

For me none of the above worked. However, ChangeDetectorRef did the trick.

From angular docs; this is how you can implement it

    class GiantList {
      constructor(private ref: ChangeDetectorRef, public dataProvider: DataListProvider) {
        ref.detach();
        setInterval(() => { this.ref.detectChanges(); }, 5000);
      }
    }

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

Baroscope answered 11/3, 2020 at 15:8 Comment(3)
seriously user will have to wait at least 5 seconds to see the changes?Ranaerancagua
@RishabhGusain ha ha ha. The 5seconds are simulating an async call like a REST Service. Feel free not to include this in your code. Of course, it is not necessary.Baroscope
Only this solution Worked for me when updating mat table datasource inside another collection. ThanksLanger
D
5

The way I found that works best is to just wrap the variable assignment in a setTimeout() function. Angular's NgZone change detection checks for changes anytime a setTimeout() function is called. Example using the code in the question:


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

    this.someService.someObservable.subscribe(
        () => console.log('success'),
        (error) => console.log('error', error),
        () => {
            setTimeout(() => { this.testVariable += '-bar'; }, 0); // Here's the line

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

    this.testVariable += '-Hello';
}
Darrel answered 22/3, 2021 at 18:10 Comment(0)
D
2

Important notice - Short way in subjects can makes problem!

I know it's not exactly what the PO asked, but there is sometimes very common problem that can happen, and maybe it will be helpful for someone.

It's very simple, and also I didn't inspect the issue deeply.

But in my case the problem was that I have used in short syntax for subscribe the subject to observable instead of full, and when I have changed it, it solved the issue.

So While this didn't work :

myServiceObservableCall.subscribe(this.myBehavSubj)

and with the same behavior that on log I am seeing the changes properly, only on the view not.

This does update the view :

myServiceObservableCall.subscribe((res)=>{
        this.myBehavSubj.next(res);
      } );

Although it's seems to be the same and the above only short-way syntax .

You are invited to explain the reason

Debrahdebrecen answered 7/7, 2020 at 11:15 Comment(3)
What the actual F*UK !?? IT WORKED for me.Indispose
the long way, but it worked for 2 mins and then didn't. I have this issue for 1 week now and can't find a solutionIndispose
@Indispose share your code in other question the community will try to help. Send me a link when you did it.Debrahdebrecen
E
2

I have been having the same issue. Initially I had to hover over the component to be able to reload the view and get the updated data from the observable.

What i ended up is changing the changeDetectionStrategy inside the component to something like this.

@Component({
    selector   : 'mycomponent',
    templateUrl: './mycomponent.html',
    changeDetection: ChangeDetectionStrategy.Default
})

I hope this helps somebody else. If you have change detection strategy changed to

@Component({
    selector   : 'mycomponent',
    templateUrl: './mycomponent.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})

Then you get the component not automatically populating the view.

Eldreeda answered 28/12, 2022 at 19:20 Comment(0)
G
0

I came here because cdkVirtualFor didn't update with new items pushed in array. So if anybody has the same issue, the source of the problem is that cdkVirtualFor will only be updated when the array is updated immutably.

Like this for example:

addNewItem(){
    let  newItem= {'name':'stack'};
    this.myItemArray = [...this.myList, newItem];
  }
Geller answered 30/6, 2021 at 8:51 Comment(0)
S
-1

All the solutions above are great. However, I found an easier solution that I believe will apply to most cases when trying to refresh the UI with new data from an external service.

The idea is pretty simple, set a loading flag, show the spinner while new data is getting fetched and then clear the spinner by resetting the flag:

getData() {
 // Set loading spinner
 this.loading = true;
 this.service.getData().subscribe((data: any) => {
   this.newData = data;
   //Remove loading spinner
   this.loading = false;
 });

}

Suave answered 12/4, 2021 at 17:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.