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.