Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get current value from Observable without subscribing (just want value one time)

How can I get the current value from an Observable without subscribing to it? I just want the current value one time and not new values as they are coming in.

like image 481
EricC Avatar asked May 20 '16 06:05

EricC


People also ask

How do you find the current value of an Observable?

To get current value of RxJS Subject or Observable, we can use the first method. const observable = of("foo"); const hasValue = (value: any) => { return value !== null && value !== undefined; }; const getValue = <T>(observable: Observable<T>): Promise<T> => { return observable.

How do you get the last emitted value from Observable?

Rather than a standard subject (Observable) that just emits values as they come in, a BehaviorSubject emits the last value upon subscribe() . You can also get the last value manually using the BehaviorSubjects getValue() method.

Do you have to subscribe to an Observable?

Sadly, no. There are at least two cases where you should explicitly subscribe to Observables in components and directives. First is when the Observable makes a change to the outside world. Most commonly, this is done by issuing a POST (or DELETE or PUT) through HTTP, meant to update the backend.

How do you find the current value of BehaviorSubject?

If you want to have a current value, use BehaviorSubject which is designed for exactly that purpose. BehaviorSubject keeps the last emitted value and emits it immediately to new subscribers. It also has a method getValue() to get the current value.


3 Answers

Quick answer:

...I just want the current value one time and not new values as they are coming in...

You will still use subscribe, but with pipe(take(1)) so it gives you one single value.

eg. myObs$.pipe(take(1)).subscribe(value => alert(value));

Also see: Comparison between first(), take(1) or single()


Longer answer:

The general rule is you should only ever get a value out of an observable with subscribe()

(or async pipe if using Angular)

BehaviorSubject definitely has its place, and when I started with RxJS I used to often do bs.value() to get a value out. As your RxJS streams propagate throughout your whole application (and that's what you want!) then it will become harder and harder to do this. Often you'll actually see .asObservable() used to 'hide' the underlying type to prevent someone from using .value() - and at first this will seem mean, but you'll start to appreciate why it's done over time. In addition you'll sooner or later need a value of something that isn't a BehaviorSubject and there won't be a way to make it so.

Back to the original question though. Especially if you don't want to 'cheat' by using a BehaviorSubject.

The better approach is always to use subscribe to get a value out.

obs$.pipe(take(1)).subscribe(value => { ....... })

OR

obs$.pipe(first()).subscribe(value => { ....... })

The difference between these two being first() will error if the stream has already completed and take(1) will not emit any observables if the stream has completed or doesn't have a value immediately available.

Note: This is considered better practice even if you are are using a BehaviorSubject.

However, if you try the above code the observable's 'value' will be 'stuck' inside the subscribe function's closure and you may well need it in the current scope. One way around this if you really have to is this:

const obsValue = undefined;
const sub = obs$.pipe(take(1)).subscribe(value => obsValue = value);
sub.unsubscribe();

// we will only have a value here if it was IMMEDIATELY available
alert(obsValue);

Important to note that the subscribe call above doesn't wait for a value. If nothing is available right away then the subscribe function won't ever get called, and I put the unsubscribe call there deliberately to prevent it 'appearing later'.

So not only does this look remarkably clumsy - it won't work for something that isn't immediately available, like a result value from an http call, but it would in fact work with a behavior subject (or more importantly something that is upstream and known to be a BehaviorSubject*, or a combineLatest that takes two BehaviorSubject values). And definitely don't go doing (obs$ as BehaviorSubject)- ugh!

This previous example is still considered a bad practice in general - it's a mess. I only do the previous code style if I want to see if a value is available immediately and be able to detect if it isn't.

Best approach

You're far better off if you can to keep everything as an observable as long as possible - and only subscribe when you absolutely need the value - and not try to 'extract' a value into a containing scope which is what I'm doing above.

eg. Lets' say we want to make a report of our animals, if your zoo is open. You might think you want the 'extracted' value of zooOpen$ like this:

Bad way

zooOpen$: Observable<boolean> = of(true);    // is the zoo open today?
bear$: Observable<string> = of('Beary');
lion$: Observable<string> = of('Liony');

runZooReport() {

   // we want to know if zoo is open!
   // this uses the approach described above

   const zooOpen: boolean = undefined;
   const sub = this.zooOpen$.subscribe(open => zooOpen = open);
   sub.unsubscribe();

   // 'zooOpen' is just a regular boolean now
   if (zooOpen) 
   {
      // now take the animals, combine them and subscribe to it
      combineLatest(this.bear$, this.lion$).subscribe(([bear, lion]) => {

          alert('Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion); 
      });
   }
   else 
   {
      alert('Sorry zoo is closed today!');
   }
}

So why is this SO BAD

  • What if zooOpen$ comes from a webservice? How will the previous example ever work? It actually wouldn't matter how fast your server is - you'd never get a value with the above code if zooOpen$ was an http observable!
  • What if you want to use this report 'outside' this function. You've now locked away the alert into this method. If you have to use the report elsewhere you'd have to duplicate this!

Good way

Instead of trying to access the value in your function, consider instead a function that creates a new Observable and doesn't even subscribe to it!

It instead returns a new observable that can be consumed 'outside'.

By keeping everything as observables and using switchMap to make decisions you can create new observables that can themselves be the source of other observables.

getZooReport() {

  return this.zooOpen$.pipe(switchMap(zooOpen => {

     if (zooOpen) {

         return combineLatest(this.bear$, this.lion$).pipe(map(([bear, lion] => {

                 // this is inside 'map' so return a regular string
                 return "Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion";
              }
          );
      }
      else {

         // this is inside 'switchMap' so *must* return an observable
         return of('Sorry the zoo is closed today!');
      }

   });
 }

The above creates a new observable so we can run it elsewhere, and pipe it more if we wish.

 const zooReport$ = this.getZooReport();
 zooReport$.pipe(take(1)).subscribe(report => {
    alert('Todays report: ' + report);
 });

 // or take it and put it into a new pipe
 const zooReportUpperCase$ = zooReport$.pipe(map(report => report.toUpperCase()));

Note the following:

  • I don't subscribe until I absolutely need to - in this case that's outside the function
  • The 'driving' observable is zooOpen$ and that uses switchMap to 'switch' to a different observable which is ultimately the one returned from getZooReport().
  • The way this works if zooOpen$ ever changes then it cancels everything and starts again inside the first switchMap. Read up about switchMap for more about that.
  • Note: The code inside switchMap must return a new observable. You can make one quickly with of('hello') - or return another observable such as combineLatest.
  • Likewise: map must just returns a regular string.

As soon I started making a mental note not to subscribe until I had to I suddenly started writing much more productive, flexible, cleaner and maintainable code.

Another final note: If you use this approach with Angular you could have the above zoo report without a single subscribe by using the | async pipe. This is a great example of the 'don't subscribe until you HAVE to' principal in practice.

// in your angular .ts file for a component
const zooReport$ = this.getZooReport();

and in your template:

<pre> {{ zooReport$ | async }} </pre>

See also my answer here:

https://stackoverflow.com/a/54209999/16940

Also not mentioned above to avoid confusion:

  • tap() may be useful sometimes to 'get the value out of an observable'. If you aren't familiar with that operator read into it. RxJS uses 'pipes' and a tap() operator is a way to 'tap into the pipe' to see what's there.

With .toPromise() / async

See 'Use toPromise() with async/await to emit the last Observable value as a Promise' in https://benlesh.medium.com/rxjs-observable-interop-with-promises-and-async-await-bebb05306875

like image 65
Simon_Weaver Avatar answered Oct 27 '22 05:10

Simon_Weaver


You need to use BehaviorSubject,

  • BehaviorSubject is similar to ReplaySubject except it only remembers the last publication.
  • BehaviorSubject also requires you to provide it a default value of T. This means that all subscribers will receive a value immediately (unless it is already completed).

It will give you the most recent value published by the Observable.

BehaviorSubject provides a getter property named value to get the most recent value passed through it.


StackBlitz

  • In this example the value 'a' is written to the console:

//Declare a Subject, you'll need to provide a default value.
const subject: BehaviorSubject<string> = new BehaviorSubject("a");

USAGE:

console.log(subject.value); // will print the current value

Conceal the Subject and only expose it's value

In case you want to conceal your BehaviorSubject and only expose it's value, let's say from a Service, you can use a getter like this.

export class YourService {
  private subject = new BehaviorSubject('random');

  public get subjectValue() {
    return this.subject.value;
  }
}
like image 35
Ankit Singh Avatar answered Oct 27 '22 03:10

Ankit Singh


const value = await this.observableMethod().toPromise();
like image 10
Sanid Sa Avatar answered Oct 27 '22 05:10

Sanid Sa