Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
584 views
in Technique[技术] by (71.8m points)

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

The title says it all really. 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...

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...