BehaviorSubject map a BehaviorSubject - rxjs

I have a rxjs#6 BehaviorSubject source$, I want get subvalue from source$
const source$ = new BehaviorSubject(someValue);
const subSource$ = source$.pipe(map(transform));
I expect the subSource$ also is a BehaviorSubject, but is not and How can I get the subSource$ is a BehaviorSubject ?

When a BehaviorSubject is piped it uses an AnonymousSubject, just like a regular Subject does. The ability to call getValue() is therefore not carried down the chain. This was a decision by the community. I agree (as do some others) that it would be nice if the ability to get the value after piping existed, but alas that is not supported.
So you would need to do something like:
const source$ = new BehaviorSubject(value);
const published$ = new BehaviorSubject(value);
const subSource$ = source$.pipe(...operators, multicast(published$));
You could then call getValue() on published$ to retrieve the value after it has passed through your operators.
Note that you would need to either call connect() on the subSource$ (which would make it a "hot" observable) or use refCount().
That said, this isn't really the most rxjs-ish way of doing things. So unless you have a specific reason for dynamically retrieving the value after it passes through your operator vs just subscribing to it in the stream, maybe rethink the approach?

Related

Should I use map or switchmap when using angular http module?

I use the following code in an angular app. I used the RxJS map call similar to how array map is used. After reading about RxJS switchmap operator, I'm not sure whether I should use map or switchmap. Should I use switchmap so the observable which is returned from the http call is closed so there is no memory leak?
getPeopleForTypeahead(term: string): Observable<IPersonForTypeahead[]> {
var peopleUrl = `https://localhost:5001/api/peoplesearch?name=${term}`;
return this.http.get<any>(peopleUrl)
.pipe(
map(pl => {
return this.peopleAsFlattened(pl.peopleList).reduce((p, c) => p.concat(c));
}),
catchError(this.handleError('getPeopleForTypeahead', []))
);
}
peopleAsFlattened = (pla: IPeopleList[]) => {
return pla.map(pl => pl.people.map(p => {
return {
id: p.id,
fullName: p.fullNames[0].firstName + " " + p.fullNames[0].lastName
};
}));
}
map and switchMap have completely different purposes:
map - transform the shape of an emission
switchMap - subscribe to an observable and emit its emissions into the stream
map
Use map when you want transform the shape of each emission. Ex: emit the user name property, instead of the entire user object:
userName$: Observable<string> = this.service.getUser(123).pipe(
map(user => user.name)
);
switchMap
Use switchMap when you want to map an emission to another observable and emit its emissions. Ex: You have an observable of some id and want to emit the resource after fetching it:
user$: Observable<User> = this.userId$.pipe(
switchMap(id => this.service.getUser(id)),
);
When user$ is subscribed to, the user returned from service.getUser(id) is emitted (not the userId string).
switchMap is not interchangeable with the map operator, nor vise versa. Although both of them has to do with mapping (as their names suggest), they have two separate use-cases.
In your particular case, the map operator is the way to go.
When to use switchMap?
You can only use switchMap(cb) when you check all these requirements:
Your callback function, cb, passed into switchMap returns an observable, observable$.
If your cb (callback function) does not return an observable, you should look into operators that don't handle higher-level observables, such as filter and map (what you actually needed); not operators that handle higher-level observables such as concatMap and well, switchMap.
You want to execute your cb sequentially before the next operation down the pipeline (after switchMap) executes.
Maybe you want to run logic inside of cb, and optionally get the return value of cb after executing, so that you can pass it down the pipeline for further processing, for example.
When you want to "discard" what will happen to cb's execution and re-execute cb every time the source observable (the thing that trickles down to switchMap(cb)) emits a new value/notification.
Applying what we hopefully learned, we know that your cb:
pl => {
return this.peopleAsFlattened(pl.peopleList).reduce((p, c) => p.concat(c));
}
returns a plain JavaScript array; not an observable. This takes using switchMap out of the question since it violates the first requirement I made up above.
Hopefully that makes sense. :)
We use switchMap when the source observable is a hot observable. In which case you prefer the behaviour that cancel the succeeding observable when source emits.
In your code, you source is a one-off http call which means it will not emit multiple times and the follow up action is not executing observable but to mutate an array. There is no need to use switchMap

Observable unsubscribe inside subscribe method

I have tried to unsubscribe within the subscribe method. It seems like it works, I haven't found an example on the internet that you can do it this way.
I know that there are many other possibilities to unsubscribe the method or to limit it with pipes. Please do not suggest any other solution, but answer why you shouldn't do that or is it a possible way ?
example:
let localSubscription = someObservable.subscribe(result => {
this.result = result;
if (localSubscription && someStatement) {
localSubscription.unsubscribe();
}
});
The problem
Sometimes the pattern you used above will work and sometimes it won't. Here are two examples, you can try to run them yourself. One will throw an error and the other will not.
const subscription = of(1,2,3,4,5).pipe(
tap(console.log)
).subscribe(v => {
if(v === 4) subscription.unsubscribe();
});
The output:
1
2
3
4
Error: Cannot access 'subscription' before initialization
Something similar:
const subscription = of(1,2,3,4,5).pipe(
tap(console.log),
delay(0)
).subscribe(v => {
if (v === 4) subscription.unsubscribe();
});
The output:
1
2
3
4
This time you don't get an error, but you also unsubscribed before the 5 was emitted from the source observable of(1,2,3,4,5)
Hidden Constraints
If you're familiar with Schedulers in RxJS, you might immediately be able to spot the extra hidden information that allows one example to work while the other doesn't.
delay (Even a delay of 0 milliseconds) returns an Observable that uses an asynchronous scheduler. This means, in effect, that the current block of code will finish execution before the delayed observable has a chance to emit.
This guarantees that in a single-threaded environment (like the Javascript runtime found in browsers currently) your subscription has been initialized.
The Solutions
1. Keep a fragile codebase
One possible solution is to just ignore common wisdom and continue to use this pattern for unsubscribing. To do so, you and anyone on your team that might use your code for reference or might someday need to maintain your code must take on the extra cognitive load of remembering which observable use the correct scheduler.
Changing how an observable transforms data in one part of your application may cause unexpected errors in every part of the application that relies on this data being supplied by an asynchronous scheduler.
For example: code that runs fine when querying a server may break when synchronously returned a cashed result. What seems like an optimization, now wreaks havoc in your codebase. When this sort of error appears, the source can be rather difficult to track down.
Finally, if ever browsers (or you're running code in Node.js) start to support multi-threaded environments, your code will either have to make do without that enhancement or be re-written.
2. Making "unsubscribe inside subscription callback" a safe pattern
Idiomatic RxJS code tries to be schedular agnostic wherever possible.
Here is how you might use the pattern above without worrying about which scheduler an observable is using. This is effectively scheduler agnostic, though it likely complicates a rather simple task much more than it needs to.
const stream = publish()(of(1,2,3,4,5));
const subscription = stream.pipe(
tap(console.log)
).subscribe(x => {
if(x === 4) subscription.unsubscribe();
});
stream.connect();
This lets you use a "unsubscribe inside a subscription" pattern safely. This will always work regardless of the scheduler and would continue to work if (for example) you put your code in a multi-threaded environment (The delay example above may break, but this will not).
3. RxJS Operators
The best solutions will be those that use operators that handle subscription/unsubscription on your behalf. They require no extra cognitive load in the best circumstances and manage to contain/manage errors relatively well (less spooky action at a distance) in the more exotic circumstances.
Most higher-order operators do this (concat, merge, concatMap, switchMap, mergeMap, ect). Other operators like take, takeUntil, takeWhile, ect let you use a more declarative style to manage subscriptions.
Where possible, these are preferable as they're all less likely to cause strange errors or confusion within a team that is using them.
The examples above re-written:
of(1,2,3,4,5).pipe(
tap(console.log)
first(v => v === 4)
).subscribe();
It's working method, but RxJS mainly recommend use async pipe in Angular. That's the perfect solution. In your example you assign result to the object property and that's not a good practice.
If you use your variable in the template, then just use async pipe. If you don't, just make it observable in that way:
private readonly result$ = someObservable.pipe(/...get exactly what you need here.../)
And then you can use your result$ in cases when you need it: in other observable or template.
Also you can use pipe(take(1)) or pipe(first()) for unsubscribing. There are also some other pipe methods allowing you unsubscribe without additional code.
There are various ways of unsubscribing data:
Method 1: Unsubscribe after subscription; (Not preferred)
let localSubscription = someObservable.subscribe(result => {
this.result = result;
}).unsubscribe();
---------------------
Method 2: If you want only first one or 2 values, use take operator or first operator
a) let localSubscription =
someObservable.pipe(take(1)).subscribe(result => {
this.result = result;
});
b) let localSubscription =
someObservable.pipe(first()).subscribe(result => {
this.result = result;
});
---------------------
Method 3: Use Subscription and unsubscribe in your ngOnDestroy();
let localSubscription =
someObservable.subscribe(result => {
this.result = result;
});
ngOnDestroy() { this.localSubscription.unsubscribe() }
----------------------
Method 4: Use Subject and takeUntil Operator and destroy in ngOnDestroy
let destroySubject: Subject<any> = new Subject();
let localSubscription =
someObservable.pipe(takeUntil(this.destroySubject)).subscribe(result => {
this.result = result;
});
ngOnDestroy() {
this.destroySubject.next();
this.destroySubject.complete();
}
I would personally prefer method 4, because you can use the same destroy subject for multiple subscriptions if you have in a single page.

Observable that behaves as BehaviorSubject(observable with default value)

I'm trying to combine latest values from 3 different streams ignoring that one(or more) of them that hasn't emit any value yet.
const myStream1$:Observable<>=// steam from somewhere,
myStream2$:Observable<string>=// steam from somewhere,
myStream3$:Observable<string>=// steam from somewhere;
Observable.combineLatest(myStream1$, myStream2$, myStream3$)
.do(([valFromStream1, valFromStream2, valFromStream3])=>
{
// I want to do some side-effect operation even if one(or all) of those 3 streams hasn't emit any value yet
})
.takeUntil(terminator$)
.subscribe();
How to achieve that?
I can achieve that using next logic(but it is as ugly as sh*t), I guess there should be an operator that does that auto-magically:
const myStream1$:Observable<string>=// steam from somewhere,
myStream2$:Observable<string>=// steam from somewhere,
myStream3$:Observable<string>=// steam from somewhere;
const subj1 = new BehaviorSubject<string>(undefined),
subj2 = new BehaviorSubject<string>(undefined),
subj3 = new BehaviorSubject<string>(undefined);
myStream1$
.do((val)=>{
subj1.next(val);
}).subscribe();
myStream2$
.do((val)=>{
subj2.next(val);
}).subscribe();
myStream3$
.do((val)=>{
subj3.next(val);
}).subscribe();
Observable.combineLatest(subj1, subj2, subj3)
.do(([valFromStream1, valFromStream2, valFromStream3])=>
{
// I want to do some side-effect operation even if one of those 3 streams hasn't emit any value yet
})
.takeUntil(terminator$)
.subscribe();
You can enforce combineLatest to emit by start each stream with a null value, then you can perform you logic conditionally base on whether the value is null
combineLatest(myStream1$.startWith(null),
myStream2$.startWith(null),
myStream3$.startWith(null))
.do(([v1,v2,v3])=>{
....
}).takeUntil(terminator$)

Does RxJs .first() operator (among others) complete the source observable?

If I have the following code:
const subject = new BehaviorSubject<[]>([]);
const observable = subject.asObservable();
subject.next([{color: 'blue'}])
observable.pipe(first()).subscribe(v => console.log(v))
According to the docs:
If called with no arguments, first emits the first value of the source Observable, then completes....
Does this mean that the source observable(the BehaviorSubject in this case) completes and you can no longer use it? As in you can no longer call .next([...]) on it.
I'm trying to understand how can an observable complete if it doesnt have the .complete() method on it?
I was trying to look at the source code of first() which under the covers uses take() and in turn take() uses lift() so I was curious if somehow first operator returns a copy of the source observable(the subject) and completes that.
The source observable is not completing, what it completes is the subscription. You could have multiple subscriptions on your Observable source, in your case one BehaviorSubject.
subject.next([{color: 'blue'}])
subject.next([{color: 'red'}])
const subs1 = observable.pipe(first()).subscribe(v => console.log(v))
const subs2 = observable.subscribe(v => console.log(v))
In the example above you clearly see that the source is not completing, just the subscription.
I have created a Stackblitz if you want to try it: https://stackblitz.com/edit/rxjs-uv6h6i
Hope I got your point!
Cheers :)

Convert Dynamic Array of Dynamic Arrays to an Observable

I have a dynamic array structure. Specifically, it is Google Maps' MVCArray. This structure has regular put, get, remove methods, as well as an addListener to listen to any changes. A library method (Polygon#getPaths) returns an MVCArray of MVCArray of LatLngs, since a polygon can have any number of paths, and each path can have any number of vertices.
My goal is to convert generate an Observable of PolygonPathEvent which will fire when either the parent MVCArray any child MVCArray will be changed.
Creating the Observables
First order of business is to convert the addListener to Observables.
private createMVCEventObservable<T>(array: MVCArray<T>): Observable<[T[], string, number, T?]>{
const eventNames = ['insert_at', 'remove_at', 'set_at'];
return fromEventPattern(
(handler: Function) => eventNames.map(evName => array.addListener(evName,
(index: number, previous?: LatLng) => this._zone.run(() => handler.apply(array, [[array.getArray(), evName, index, previous]])))),
(handler: Function, evListeners: MapsEventListener[]) => evListeners.forEach(evListener => evListener.remove()));
}
Combining the Observables
Now to combine, seemingly we need to use combineLatest
const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
return combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths);
);
The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array. So when there is a new path added, I don't know how to make the returned Observable also listen to this new path. Same, if a path is deleted, I don't know how to make the returned observable unsubscribe from the deleted path.
Piping to Subject (Wrong approach)
I thought about returning a Subject, and simply subscribing it to different observables whenever the parent MVCArray<MVCArray<LatLng>> changes.
const retVal: Subject<PolygonPathEvent> = new Subject();
const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
let latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths)
).subscribe(retVal);
pathsChanges$.pipe(tap( ([arrays, event, index, previous]) => {
latestSubscription.unsubscribe();
latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) =>
new PolygonPathEvent(pathArr, paths)
).subscribe(retVal);
} ));
return retVal;
This works, the problem is that a subscription to the original Observables (and addListener) happens right in this method, and not when the returned Observable is subscribed to.
Conclusion
I need some kind of operator for this.
If I understand your problem right, there may be space to use switchMap to solve it.
You say that "The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array". I think that the static array you refer to is the one emitted by the pathChanges$ Observable, which is created once for all at the beginning of your code snippet and gets not updated when the parent MVCArray of MVCArray changes.
If my understanding is right, what we need to do is to provide a way to notify the event of change of the parent MVCArray of MVCArray and, any time such event occurs, execute again the combineLatest function using the new updated array.
This can be accomplished with a logic similar to the following
const pathsChanges$ = this.createMVCEventObservable(paths);
pathsChanges$
.pipe(
switchMap(() => combineLatest(paths.getArray().map(this.createMVCEventObservable))),
map(pathArr => new PolygonPathEvent(pathArr, paths))
)
This logic assumes that the variable paths is passed into this piece of logic from the outside.
I could not reproduce the case and therefore I am far from sure that my answer solves your problem, even if I hope it helps.

Resources