angular: AsyncPipe doesn't work with RxJs Subject

I’m submitting a … (check one with “x”)

[X] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior AsyncPipe works properly with BehaviorSubject, but it don’t work with Rx Subject. More info: http://stackoverflow.com/questions/39902413/angular-2-0-1-asyncpipe-doesnt-work-with-rx-subject

Expected behavior Works with Subject

Minimal reproduction of the problem with instructions

export class AppComponent {
  foo = new Subject<Array<number>>();

  constructor(){
     setTimeout(() => {
          this.foo.next([1,2,3]);
     }, 3000);
  }

  getFoo(){
     return this.foo.asObservable();
  }
}

<span *ngFor="let e of foo.asObservable() | async">{{e}}</span>

What is the motivation / use case for changing the behavior? I use Subject because I somethines need subscribe to the service when data is loaded an make decisions. BehaviorSubject forces me to initialize the object with an empty data.

Please tell us about your environment: Not required

  • Angular version: 2.0.X Angular 2.0.1
  • Browser: All
  • Language: all
  • Node (for AoT issues):
    4.4.4

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 6
  • Comments: 16 (9 by maintainers)

Most upvoted comments

Closing because we won’t document such a narrow use case in the general Angular docs. This would be better as a Stack Overflow Q&A

The problem here is calling a method from your template - this means every time change detection runs, you’re calling your getFoo() function, which returns a new instance of the observable, which resets the async pipe.

As @DzmitryShylovich’s example shows, its generally preferable to bind to properties on your component:

export class AppComponent {
  foo = new Subject<number[]>();
  foo$ = Observable<number[]>
  constructor(){
    this.foo$ = this.foo.asObservable();
     setTimeout(() => {
          this.foo.next([1,2,3]);
     }, 3000);
  }
}
<span *ngFor="let e of foo$ | async">{{e}}</span>

In my project. I had a service with a collection wrapped by BehaviorSubject. To retrieve the collection out of the service I used the following method getData(): Observable { return this._data.asObservable() }. Then I inject the service into components as a component variable and used the method getData() directly in the ngFor. At this point everything worked properly.

The problem came out when I changed the word BehaviorSubject for Subject and the code failed. I tried to subscribe to service in the component this._service.getData().subscribe(data => console.log(data)) and I could check the data was arriving to this point. So I concluded it was a async pipe problem.

@robwormald The weird behavior is that all of this don’t happen with BehaviorSubject, maybe cause it’s initialized at the beginning.

@DzmitryShylovich Try removing the setTimeout, it wont work anymore.

Something along the lines of nexting on the subject while it has not been subscribed to yet causes this bug. It gets subscribed to when the view is initialized, therefore I think the moment the view is initialized is related.

See this example for a test of all the possible ways I could think of someone would use the async pipe in combination rxjs.

So I still do not understand. I too have an async pipe which works with BehaviorSubject but not with Subject. What should I do? Just convert to a BehaviorSubject? Is using ngAfterViewInit a good approach?

@realappie I can confirm this behavior. Took me almost all day to realize this wasn’t a bug in my own approach.

service.ts

getSubject = new Subject<T[]>();

// get all elements
public get(): Observable<T[]> {
  return this.getSubject.pipe(
    switchMap( params => this.http.get<T[]>( `${this.API_BASE_URL}`, this.getHttpOptions( params ) ) ),
    map( response => this.jsonConvert.deserialize( response, this.type ) )
  );
}

public refresh( params?: any ) {
  this.getSubject.next( params );
}

component.ts

constructor(private service: Service) {
    this.elements = this.service.get();
}

ngOnInit() {
  // does not produce any results without a slight timeout on the refresh method
  setTimeout(() => {
    this.service.refresh();
  });
}

component.html

<div *ngFor="let e of elements | async"

@briancodes months after, I admit it’s reasonable to say knowledge of the various Subjects implementations is needed to understand the use-cases and the effects produced of each one. I did not have a sufficient understanding back then.

@lppedd If a Subject emits a value with next, an async pipe will only handle this event if its already subscribed to the Subject. What happens in the Angular template is that the async pipe subscription can occur after next has been invoked. Subscribing late to the Subject means it misses the previous event

With a BehaviourSubject or ReplaySubject, a late subscription still receives the previous event. That’s why they work more consistently with async pipe