Lorraine Ram-El
Lorraine Ram-El

Reputation: 2755

Simple way to get current value of an Observable (Obtained from a BehaviorSubject)

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!

Upvotes: 2

Views: 3651

Answers (3)

ChrisY
ChrisY

Reputation: 1783

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

Upvotes: 1

D Pro
D Pro

Reputation: 1776

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)

Upvotes: 2

hevans900
hevans900

Reputation: 897

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).

Upvotes: 2

Related Questions