Async not working for Subject.asObservable

I’ve come across what appears to be an access issue. I have a Subject in a service, shared via a public accessor as an observable.

  // can't subscribe with async
  private _connection$: Subject<number> = new Subject();
  public get connection$(): Observable<number> {
    return this._connection$.asObservable();
  }
  // this one works as I'd expect it
  private _interval$ = interval(1000);
  public get interval$(): Observable<number> {
    return this._interval$;
  }

When I subscribe to connection$ in a component controller I can access the emissions in the UI. However when I try to directly access it with an async pipe, I can’t. I added another observable as a property to the service and I can directly access that using async.

<div>from shared obs$: {{ data.interval$ | async }} - works</div>
<div>from async: {{ data.connection$ | async }} - doesn't work</div>
<div>from observable: {{ fromObservable$ | async }} - works</div>
<div>from subscription: {{ fromSubscription }} - works</div>
  public fromSubscription: number = 0;
  public fromObservable$: Observable<number> = this.data.connection$;

  constructor(public data: DataService) {
    data.connection$.subscribe({
      next: (count) => (this.fromSubscription = count),
    });
  }

I can work around it. But I’d like to understand why this is the case. It seems that the Subject returned asObservable in the service accessor isn’t subscribable via async but a regular observable is.

For what I have seen u r using imho an unusual (or even wrong) pattern to show content in the template

Imho u r not supposed to directly reference an injected service into the template. The template should only reference to declared variables in the component class. And using getter/methods in the template imho is a big no-no due to performance effects and change detection

Next there seems an odd pattern of referencing to a subscription within the subscription using a next. Either there is something I can learn or u r very creative in the wrong way!

:grinning:

May I suggest you to do the Tour of Heroes example on angular.io? At least in case u like to learn angular very opionated view on coding.

asObservable is IMHO pointless. Simply returning the Subject out of a method that declares a return value of an Observable is sufficient to achieve the desired goal. No client code can see the Subject-y part, such as next, without a cast.

Thank you for your insights. The stackblitz code is a contrived example of the behaviour I’m seeing. It’s not trying to do anything, it’s just attempting to show you something.

The tour of heroes tutorial you yourself reference, accesses injected service methods/properties directly from a component template. This would suggest it isn’t an anti-pattern.

  <div *ngFor='let message of messageService.messages'> {{message}} </div>

This template binds directly to the component’s messageService.

An Angular event binding binds the button’s click event to MessageService.clear()."

Having said that I am interested in knowing what you suggest as an alternative. I take your point that getters/methods shouldn’t be referenced in a template. But getters are minimalistic by design and in this case I’m not sure how to avoid it. I don’t want to directly reference the Subject. _connection$ is being updated by a separate service. And I want the UI to change based on its current state.

I’m moving away from utilising a state machine and wanted to directly tap the observable. Perhaps I’ll retain the FSM and update that with subscription. I have the notion that each component should be able to independently act on emissions from the data service _connection$ Subject. It comprises a couple of different sources so it makes good sense to have it in a shared service. Letting Angular manage the subscription in the template where it’s only function is to update same, seems to be the right thing.

Moreover, without digging into how aync subscriptions work, I had hoped it would access the getter once, retrieve the observable and wait for an emission. I didn’t think that it would ask for a new observable on each cycle.

Do you mean?

next: (count) => (this.fromSubscription = count)

It’s not what you think. I’m assigning the emissions of an observable to a poorly named numeric property. I did that to illustrate a component side subscription to the service observable that’s accessible from the template. That’s just troubleshooting.

Oh, do mean this bit in the stackblitz?

next: (i) => this._connection$.next(i)

It’s contrived, the actual inputs to the Subject come from UI interaction and bluetooth events. That was just a quick and nasty way of pumping mock data into the Subject.

I return the Subject asObservable, as you’ve alluded, to hide methods like next and avoid side effects. And I’m using a Subject to multicast information to multiple observers. There generally seems to be a lot of extraneous helper methods/operators in the rxjs space. And we managed to achieve all of the same functionality (to varying degrees) before adopting a declarative style of coding! :wink:

Nevertheless, it feels more idiomatic to use them where appropriate. In defence of asObservable, it makes the code look more deliberate. And if you trusted that the Subject won’t be abused further down the line, then why not just return it as a Subject and avoid the cast altogether?

So… any idea why async doesn’t pick up the stream?

Oops- ok. You are right. I think I rather declare a public component variable and assign value to it, knowing I generally need to do (declarative) stuff on it anyway in the future - which I don’t want in the template. And I like to keep the template as simple as possible, with ideally no business logic. So I guess I taught myself not to do this as principle.

Interestingly, if you change the Subject to BehaviorSubject, it does work :slight_smile:

import { Injectable } from '@angular/core';
import { interval, Observable, Subject, BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  constructor() {
    interval(1000).subscribe({
      next: (i) => this._connection$.next(i),
    });
  }

  private _connection$: Subject<number> = new BehaviorSubject(0);
  public get connection$(): Observable<number> {
    return this._connection$.asObservable();
  }

  private _interval$ = interval(1000);
  public get interval$(): Observable<number> {
    return this._interval$;
  }
}

That does not answer your question, why it doesn’t in your case. And also why fromObservable does work.

To your other point - this I would use as opposed the “next” syntax - maybe also a personal preference

  constructor(public data: DataService) {
    data.connection$.subscribe(
     (count) => {
        this.fromSubscription = count;
    });
  }

And another experiment - not using getter but simple method does not help either in solving. So the getter isn’t likely an issue.

And this is working too (with Subject):

  private _connection$: Subject<number> = new Subject();
  public get connection$(): Observable<number> {
    return this._connection$ as Observable<number>;
  }

or a really wrong way to expose streams:

  private _connection$: Subject<number> = new Subject();
  public get connection$(): Observable<number> {
    return this._connection$; //  as Observable<number>;
  }

Making the asObservable() suspicious…

Anyway, I guess you are having some more pointers on how to isolate and define your appetite for understanding more why things are not working.

Still no explanation from my end, but for me the curiosity loses from other priorities.

I do like to learn why though!!!

Yes, and this is what I’ll likely do. The observable is used in other ways in the component. It feels wrong to refer to the service getter observable for those and to the local component variable just for the template. But I can’t use that for other actions I want to hang off the observable because it won’t multicast. Hense the Subject.

I have that situation with another observable I use :slight_smile: It’s curious for sure. It could be that it has an initial value when the async pipe is accessing the getter and returning the observable. I’ve since discovered that async is an impure pipe and has some form of internal polling to check for emitted values.

fromObservable creates a public component level variable and assigns it to the observable returned by the getter. I mean, all of the permutations should work. It might just be an edge case bug rather than a logical limitation to be reasoned about.

Oh look at that, it’s mentioned in the Angular github issues.

It’s suggested in this situation that the getter is returning a fresh instance of the observable each time change detection is run. This would reset the async pipe.

This doesn’t really make sense. Although let me check something. I tapped the observable in the getter body.

return this._connection$.asObservable().pipe(tap((i)=>console.log(i)));

And I removed all calls to connection$ except the async pipe. When I swapped out the direct call for a component scopped observable (fromObservable$) the console output was 0,1,2 as expected and the web component was updated. With just the async directly accessing the service the console output was correct (0,1,2,3) but nothing showed in the html. The pipe therefore correctly subscribed to the observable and a single instance of it. But it didn’t return the value to the page.

It’s also suggested that the reason the BehaviorSubject works differently is that it is initialised prior to next being called on the Subject. I’ve wrapped the interval subscription in a setTimeout to check this and it makes no difference. But they were dealing with a single value emitting observable so indeed it would have been missed with a late subscription from async.

1 Like

My key take-awake: keep the template easy. Just public component variables for binding. And for method calls - invoke the method, no logic in the template

Change detection is a blessing and a real pain the bum if u try to be too smart.

Thanks for sharing - was a nice and usefull way to break away from daily routine!

1 Like

As long as it’s a conscious decision, I’m cool. Personally, I quit using asObservable when I noticed that the Angular internals just return Subjects as Observables.

The only way that it could be abused any further down the line would be by with an unsafe downcast to Subject, because as far as tsc is concerned, it’s an Observable when it is returned from a function declaring an Observable return type. If your coding guidelines mandate believing interface contracts, no client code could ever be justified in casting it to a Subject.

Long-time readers of this column are familiar with my general distaste for JavaScript. One thing I hate about it is lack of proper access control, but we work with what we have. Since it does lack any access control at the language level, I don’t think there’s much point in attempting to bolt it on at the app level. I figure anybody sufficiently motivated to peek at the source and downcast something promised only to be an Observable to a Subject and call next on it would not bat an eye at doing the same to the source member of the facade that asObservable returns, so any additional protection against abuse looks pretty illusory to me. YMMV.

1 Like

Happy Cake Day!

I really do try. I also want to keep the component code simple. Ergo subscribing to the same shared observable from code and component. Now I have the added overhead of assigning a component property to an otherwise available service one. And exclusively for this use case. Elsewhere in the code in the same component I directly access the service property. It’s superfluous. I’m probably gonna have to leave a comment.

// No one reads comments.

I’ve been accused of worse.

1 Like

Nahh…no accusation. To me it is the overly extreme flexibility of JS and the consequences it has for people using frameworks that, only to a limited extend, can compensate for this JS flexibility (read: JS stupidity).

Happy cake day and happy coding!

Speaking of which; I’m currently swapping out perfectly good function calling clickable elements with fromEvent observable streams. Am I mad? Should I just stop!?

It’s your cake day Tommertom, not mine, see? See the cake there beside your name? See?!
cake

Ah yeah. anniversary in this forum. Thx!

no further recommendations :slight_smile:

FWIW, here’s what I typically do in situations like this. The AsyncPipe seems cool at first blush, but I find it difficult to debug, and prefer other solutions to the same general resource leak problem that it’s trying to address (like @ngneat/until-destroy).

So (apologies for syntax errors, I haven’t written any TypeScript in a few months and have got a serious case of “Kotlin brain”):

class FooService {
  private all$ = new BehaviorSubject<Foo[]>([]);
  watchAll(): Observable<Foo[]> { return all$; }
}

class FooPage {
  allFoos: Foo[] = [];
  constructor(private foosvc: FooService) {
    foosvc.watchAll().pipe(untilDestroyed(this))
      .subscribe(foos => this.allFoos = foos);
  }
}

<div ngFor="let foo of allFoos">...</div>