1

This question looks long, but it should be a very basic rxjs question. Please help if you can.

I'm writing an Angular 7 app, with a service, component and a template as follows, and I want to access the last value of the observable in an easy way.

SoundMeterService

In the service, I have a private BehaviorSubject which I use to emit values using 'next'. I expose this BehaviorSubject to consumers using .asObservable() in order to prevent consumers from doing .next() and emit new values. In the service, I access the current value of listeningStatusSubject using .value.

export type SoundMeterStatus    = 'noResults' | 'isRunning' | 'hasResults';

export class SoundMeterService {
    private listeningStatusSubject: BehaviorSubject<SoundMeterStatus> = new BehaviorSubject<SoundMeterStatus>('noResults');
    get listeningStatus()   { return this.listeningStatusSubject.asObservable(); }

    // somewhere in the code I emit a different value
    someFunc() {
        if (this.listeningStatusSubject.value === 'noResults' )
            this.listeningStatusSubject.next('isRunning');
    }
}

SoundMeterComponent

Following best practices with observables, in the component, I'm using direct access to the service's Observable (via listeningStatus getter) so that it is automatically subscribed to and unsubscribed in the template (see below).

export class SoundMeterComponent {
    serviceListeningStatus$ = this.sms.listeningStatus;

    constructor(
        private sms: SoundMeterService
    ) {}

    ngOnInit() {
        // I want to check the value of listeningStatus in a simple way without pulling the value out and saving it locally in SoundMeterComponent
    }
}

SoundMeterTemplate

I'm using Angular's async pipe so that it automatically subscribes and unsubscribes to the observable when the component is destroyed (to avoid manual subscriptions as mentioned here)

<div *ngIf="(serviceListeningStatus | async) === 'noResults' ">
    <span>some text</span>
</div>

My question

This approach works perfectly. But I want to check the value of listeningStatus in a simple way inside ngOnInit() without pulling the value out and saving it locally in SoundMeterComponent, and without exposing the BehaviorSubject listeningStatusSubject itself to check its value using .value, as described below:

1) Pulling the value from the service and saving it locally in SoundMeterComponent:

SoundMeterComponent

export class SoundMeterComponent {
        serviceListeningStatus$ = this.sms.listeningStatus;
        private serviceListeningStatus;

        constructor(
            private sms: SoundMeterService
        ) {}

        ngOnInit() {
            // Pulling the value from the service:
            this.sms.listeningStatus.subscribe( status => this.serviceListeningStatus = status ); // I'm trying to avoid this

            if (this.serviceListeningStatus === 'noResults' ) {
                displayNoResultsMessage();
            }
        }
    }

2) exposing the BehaviorSubject itself and check its value using .value like this:

SoundMeterService

export class SoundMeterService {
    private listeningStatus: BehaviorSubject<SoundMeterStatus> = new BehaviorSubject<SoundMeterStatus>('noResults');
    get listeningStatus()   { return this.listeningStatus; }

}

SoundMeterComponent

export class SoundMeterComponent {
    serviceListeningStatus = this.sms.listeningStatus;

    constructor(
        private sms: SoundMeterService
    ) {}

    ngOnInit() {
        if (this.sms.listeningStatus === 'noResults' ) {
            displayNoResultsMessage();
        }
    }
}

SoundMeterTemplate

<div *ngIf="(serviceListeningStatus.asObservable() | async) === 'noResults' ">
    <span>some text</span>
</div>

Please help me understand which approach is better while keeping clean and simple code. I know there are many questions on how to get the value from a BehaviorSubject like Simple way to get the current value of a BehaviorSubject with rxjs5 or How to get current value of RxJS Subject or Observable? , but please note that this is a different question. Thanks!

Lorraine R.
  • 1,545
  • 1
  • 14
  • 39
  • a question that arises: do we really want to check the status just in the `onInit`? Can you be sure that when `ngOninit` is called that the result is already settled? Otherwise, since we are subscribing in the template via async pipe, we may could use the `pipe` & `tap` operator on the `listeningStatus` observable in the component and perform the `displayNoResultMessage()` side effect in it. – ChrisY Sep 17 '19 at 14:46
  • Originally it's a BehaviorSubject (i.e has an initial value), so yes, it is safe to use it in ngOnInit. In fact, I use `if(this.serviceListeningStatus === ... )` in other places in the code where I expect `listeningStatus` to be already changed. Do you think that I still need to pull the value and save it locally if I use it in 3 other places in the code? – Lorraine R. Sep 18 '19 at 04:05
  • 1
    Why don't you just expose the current status in the SoundMeterService (using .getValue() on the behavior subject to return the concrete value) ? – Roddy of the Frozen Peas Sep 18 '19 at 21:09

3 Answers3

2

Can always use async/await in combination with ngOnInit & converting the observable to a promise, to give you some readability:

async ngOnInit = () => {
  const currentValue = await this.sms.listeningStatus.pipe(take(1)).toPromise();

  // do stuff with currentValue here
}

(note: always use take(1) when converting hot observables to promises - or they'll never resolve, as you need the observable to complete before the promise will resolve).

hevans900
  • 847
  • 7
  • 15
2

I'm afraid you have to subscribe to this.sms.listeningStatus, that's just how observables work. There is no workaround if you want to stick to real asynchronous approach of dealing with data. The only thing you could do is move the subscription somewhere else (for instance to the template using async pipe)

D Pro
  • 1,756
  • 1
  • 8
  • 15
1

As far as I can understand your problem I would propably question the usage of a BehaviourSubject in the first place. RxJs, as great as it is, is not a fit for every use case and yours could be one of them. The usage of .value is an indication for that imo.

Using a simple non Observable variable in the SoundMeterService that keeps the state would propably resolve all your problems.

If you don't want to do that, my suggestions would be to try something like the following: There a several places in your flow where you want to check the listeningStatus and it may is an option for you to use the tap operator and perform the side effects for all options (not only for 'noResults') in one place?

Example:

export class SoundMeterComponent {
    serviceListeningStatus$ = this.sms.listeningStatus.pipe(
        tap((status) => {
            if (value === 'noResults') {
                displayNoResultsMessage();
            }
            if (value === 'xxx') {
                doXXXStuff();
            }
        })
    );

    constructor(private sms: SoundMeterService) {}

    ngOnInit() {}
}

and the template then subscribes to the serviceListeningStatus$:

<div *ngIf="(serviceListeningStatus$ | async) === 'noResults' ">
    <span>some text</span>
</div>

I think your problem is a great example for an application that wants to do RxJs, because it is great, but due the lack of a full support of reactive approaches in the Angular Framework you can't go fully reactive. I'm thinking of the ngOnInit or of click events a user makes. If you want to use RxJs nonetheless it may help to take look at ngx-template-stream or at this great proposal.

Cheers Chris

ChrisY
  • 1,681
  • 10
  • 12