4

Repro

https://stackblitz.com/edit/angular-rlqkyb

app.service

Simple service creating a BehaviorSubject with some default values, a function to return this subject as an observable and finally a function to emit a new value on the subject.

app.component

This component simply gets a reference to the observable returned by the service and subscribe to it via the async pipe. Note that the observable is piped in order to display the emitted value.

This component also displays the second component.

app2.component

This component has an input that subscribes to the observable returned by the service that emits a new value and that's it.

The problem

The problem is that even though I see the new value emitted in the console, the view of app.component is not updated.

At first, I thought that it was because the observable returned by the service was not in the zone, so I tried wrapping some part of this function into a zone.run (the whole content of the observable and just the next/complete calls) but it still didn't work.

One thing that works is the commented line in app2.component that subscribes to the observable using a setTimeout.

Another thing that works is to set the change detection strategy to default instead of OnPush, however, I do not wish to do so.

In my mind, the app.component gets the observable, subscribes to it via the async pipe (so it should refresh the value as soon as it receives a new value, that's the goal of the observable, indicating to Angular that something has changed). At the same time, app2.component gets loaded and via its input, it updates the observable, so the async pipe of app.component should get the new value and update its view as it is doing without the OnPush strategy.

I'm pretty confident it's not an angular issue but rather a misunderstanding from me, but then I'd appreciate if someone could explain me where I'm wrong :-)

Note that I also checked the other thread on StackOverflow but they all talk about zone.run or using detectChanges (which I wouldn't know where to use it here)

ssougnez
  • 5,315
  • 11
  • 46
  • 79
  • Why the complicated setData..? You can simply next the observable there..? – MikeOne Jun 15 '22 at 16:50
  • I could indeed but this is an oversimplification of are reactive store that I am implementing and in it, the setData does way more than simply doing a next. – ssougnez Jun 15 '22 at 16:52
  • That could be the case. However, you’re seeing an issue now, so I would start by simplifying stuff first? – MikeOne Jun 15 '22 at 16:53
  • The code in itself is not complicated. I prefer the approach where I understand what fails in order to be able to improve the code accordingly. However, I'll still have a look to see if this can be improved. – ssougnez Jun 15 '22 at 17:02

2 Answers2

4

The emission of a new value from the observable marks the OnPush component to be checked in the next change detection cycle. But I think in your case, there is simply nothing happening that triggers a CD cycle in the application. That explains why the line with the setTimeout fixes it: setTimeout triggers a CD cycle and because the component is marked to be checked, it is updated.

slim
  • 414
  • 1
  • 7
  • I think you probably nailed it.. that must be it. Not a real-life scenario this it seems? – MikeOne Jun 15 '22 at 17:01
  • 1
    You're right, il from Mars and I'm trying to find weird scénario not happening in the real life to make expert think a lot ;-) thanks for the answer, it would actually make sense. – ssougnez Jun 15 '22 at 17:06
  • @ssougnez … let’s just say, I’ve never encountered this specific scenario. I wonder if the result will be the same if you’d use something like NgRx. Wouldn’t surprise me If it also won’t update the dom. – MikeOne Jun 15 '22 at 19:04
  • In two words. Say that app display a list of partially loaded item. When clicking on a button, it updates a variable called "selectedId" bound to the details component. In the setter of the input of the details component, you call function that fully load the object but set it as "loading" before the http call. In this case, the main component won't "see" that the loading property has changed even tough the `tap` display that the value has been emitted. – ssougnez Jun 15 '22 at 19:37
  • Hm, I guess if you bind the loading property via an async pipe, it should still work: Clicking a button triggers CD -> because the input changed, the component gets checked, the setter is executed, loading$ emits true, view is updated. A finished http call also triggers CD afaik, loading$ emits false and thus marks the component to be checked in this CD -> the view is updated. – slim Jun 15 '22 at 19:51
  • It doesn't seem to matter if the value is a primitive or `Observable`. If `next` is called outside of the zone, Angular doesn't care. I thought using observables in templates are automatically subscribed and UI updated regardless if it's change outside the zone. I wish there was an easy way to tell Angular to run everything in the template in zone. Especially for observables. Or a pipe to wrap/run in zone. Currently I have to do this manually in ugliest way in ts. – Domske Nov 23 '22 at 12:44
1

Add .slice(), angular will refresh.

let objects = this.state.objects.slice();
this.state = {...this.state, objects};
nate-kumar
  • 1,675
  • 2
  • 8
  • 15