One time operation on First subscription of observable - javascript

I am using Rxjs. I have one observable and multiple subscription from different sources. I wish to trigger one function only once after getting first subscription to the observable. Is there any way to achieve this?

Not sure if this fits your scenario, but I have dealt with this in the past as well and found it best to conditionally return a different observable after checking for an initialization variable of some kind. Below is a working example of what I mean.
Component wants a list of states from an API
this.statesService.getStates()
.subscribe((states) => this.states = states);
Service wants to only get the states once from the API
private _states: IState[];
getStates(): Observable<IState[]> {
if (!this._states) {
// we don't have states yet, so return an observable using http
// to get them from the API
// then store them locally using tap
return this.http.get<IState[]>('/options/states').pipe(
tap((answer) => {
this._states = answer;
}),
);
} else {
// subsequent calls will just return an observable of the data needed
return of(this._states);
}
}
In the case above, it's easy to return a conditional observable. Hopefully this provides you some ideas on how to handle your conditional (only on the first subscribe) scenario.

You can use publishReplay(1), refCount() operators. It will ensure to evaluate observable only once and share the same result to all subscribers.

Related

In Angular multiple http requests fire when I redirect from one component to another component (in increasing order), do we have any solution for it?

In my project there is one API which is called on init method when component is called as follows:
ngOnInit(): void {
this.getCurrentUserpost();
}
and the function looks like this
getCurrentUserpost(){
this.friend.getAllFriendsPost(this.friendsId).subscribe(res => {
this.allPosts = res;
});
}
and the service API function looks like the following
getAllFriendsPost(ids:any){
return this.http.post(this.BaseUrl+'/allfriendspost',ids);
}
When I navigate between component using routeLink then every time when I visit component where I have added this API, it will call the API multiple times at the same time with increasing order.
See the network tap where you can see multiple API calls are done
Can anyone advise me how to I can resolve this issue?
You have a memory leak. You need to unsubscribe from the observable.
One of multiple possibilities would be to use take(1) rxjs-operator.
getCurrentUserpost(){
this.friend.getAllFriendsPost(this.friendsId).pipe(
take(1),
).subscribe(res => {
this.allPosts = res;
});
}
Just to mention some other possibilities:
async pipe
takeUntil operator (or other take*) operators
Subscription.unsubscribe

Modyfing data in an observable

I have the following observable: messages$: Observable<Message[] | undefined>. Message has 2 fields: id and content, both of which are string.
What I would like to do is to modify messages$ so that a function foo(string) is invoked on the content of each Message.
It doesn't seem difficult at face value but I'm new to observables and unfortunately I got stuck.
I guess solution is simple:
messages$: Observable<Message[] | undefined> = yourSource
.pipe(
map(messages => {
messages.forEach(value => {
value.content = foo(value.content);
});
return messages;
}
)
What you are asking is how can you change your Observable to an observable with sideeffect. You probably don't ever want that (except for simple cases like logging stuff).
Instead what you want to do is subscribe to that Observable and then do your logic in the subscription. That way you're also guaranteed that your logic is only run once (or the number you want) instead of being reliant on something else subscribing to the observable.
messages$.subscribe(({ content }) => { foo(content); });
Be careful of subscription that is not unsubscribed.
Check out this question for a solution to that generic problem:
RXJS - Angular - unsubscribe from Subjects
If i misunderstood your question, and what you really want is an observable that transforms the data, and your foo method is pure (does not modify the inputs or other external data), the solution is different:
const modifiedMessages$ = messages$.pipe(map(({ content }) => foo(content));

Caching observables causing problem with mergeMap

I have a caching method in a container:
get(): Observable<T[]> {
if (!this.get$) {
this.get$ = merge(
this.behaviorSubject.asObservable(),
this._config.get().pipe(shareReplay(1), tap(x => this.behaviorSubject.next(x))));
}
return this.get$;
}
This works fine with normal observables, however when I cache the bellow in a myContainer2 (e.g using cached observable's result to create another cached observable) method like:
// get is assigned to _config.get in the above function
const myContainer2 = new Container({get: () => myContainer1.get().pipe(mergeMap(res1 => getObs2(res1))});
// please note, the end goal is to resolve the first observable on the first subscription
// and not when caching it in the above method (using cold observables)
myContainer2.get().subscribe(...) // getObs2 gets called
myContainer2.get().subscribe(...) // getObs2 gets called again
myContainer2.get().subscribe(...) // getObs2 gets called for a third time, and so on
every time when the second cache is subscribed to getObs2 gets called (it caches nothing).
I suspect my implementation of get is faulty, since I am merging an behavior subject (which emits at the beginning), but I cant think of any other way to implement it (in order to use cold observables).
Please note that if I use normal observable instead of myContainer.get() everything works as expected.
Do you know where the problem lies?
Using a declarative approach, you can handle caching as follows:
// Declare the Observable that retrieves the set of
// configuration data and shares it.
config$ = this._config.get().pipe(shareReplay(1));
When subscribed to config$, the above code will automatically go get the configuration if it's not already been retrieved or return the retrieved configuration.
I'm not clear on what the BehaviorSubject code is for in your example. If it was to hold the emitted config data, it's not necessary as the config$ will provide it.

Performance of an angular 2 application with Firebase

I have been creating a web application using angular2 with firebase (angularfire2),
I want to know if my development method is optimized or not.
When user select a group, I check if he is already member of the group.
ngOnInit() {
this.af.auth.subscribe(auth => {
if(auth) {
this.userConnected = auth;
}
});
this.router.params.subscribe(params=>{
this.idgroup=params['idgroup'];
});
this._groupService.getGroupById(this.idgroup).subscribe(
(group)=>{
this.group=group;
this.AlreadyPaticipe(this.group.id,this.userConnected.uid),
}
);
}
this method is work, but when I place the function AlreadyPaticipe(this.group.id,this.userConnected.uid) outside getGroupById(this.idgroup).subscribe() ,I get an error group is undefinded ,I now because angular is asynchrone. I don't khow how I can do it?. How I can optimize my code ?,How I can place the function AlreadyPaticipe(this.group.id,this.userConnected.uid) outside getGroupById(this.idgroup).subscribe()
Thanks in advance.
Everything as stream :
Well first, you shouldn't subscribe that much, the best practice is to combine your observables into one and subscribe to it just once, because everytime you subscribe, you need to cleanup when your component is destroyed (not for http, neither ActivatedRoute though) and you end up managing your subscription imperatively (which is not the aim of RXjs). You can find a good article on this topic here.
You must think everything as a stream, all your properties are observables :
this.user$ = this.af.auth.share(); //not sure of the share, I don't know firebase, don't know what it implies...
this.group$ = this.router.params.map(params => params["idgroup"])
.switchMap(groupID => this.groupService.getGroupById(groupID)).share();
// I imagine that AlreadyPaticipe return true or false, but maybe i'm wrong
this.isMemberOfGroup$ = Observable.combineLatest(
this.group$,
this.user$.filter(user => user !== null)
).flatMap(([group, user]) => this.AlreadyPaticipe(groupID, user.uid));
You don't even have to subscribe ! in your template you just need to use the async pipe. for example:
<span>user: {{user$|async}}</span>
<span>group : {{group$|async}}</span>
<span>member of group : {{isMemberOfGroup$|async}}</span>
Or if you don't want to use the pipe, you can combine all those observable and subscribe only once :
this.subscription = Observable.combineLatest(
this.group$,
this.user$,
this.isMemberOfGroup$
).do(([group, user, memberofGroup]) => {
this.group = group;
this.user = user;
this.isMemberofGroup = memberofGroup;
}).subscribe()
in this case, don't forget to this.subscription.unsubscribe() in ngOnDestroy()
there is a very handy tool on rxJS docs (at the bottom of the page) that helps you to choose the right operator for the right behavior.
I don't care about streams, I want it to work, quick n' dirty :
If You don't want to change your code too much, you could use a Resolve guard that will fetch the data before your component is loaded. Take a look at the docs:
In summary, you want to delay rendering the routed component until all necessary data have been fetched.
You need a resolver.

How to cleanly reconnect to a ReplaySubject while avoiding past memoization side-effects?

I hold state in one ReplaySubject that replays the last copy of the state. From that state, other ReplaySubjects are derived to hold...well, derived state. Each replay subject need only hold it's last calculated state/derived state. (We don't use BehaviorSubjects because they always give a value, but we only want a value derived from our parent observables.) It is always necessary to replay the value to new subscribers if we have already generated derived state.
I have a custom observable operator that accomplishes this in just the way I want it to, but it doesn't feel that clean. I feel like there should be an efficient way to accomplish this with RxJ's operators themselves.
I have tried the two most obvious approaches, but there are slight problems with each. The problem involves unsubscribing and re-subscribing.
Open the fiddle below, open your console, and click run. I will describe the problem with each output.
https://jsfiddle.net/gfe1nryp/1/
The problem with a refCounted ReplaySubject
=== RefCounted Observable ===
Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Resubscribe
Subscription 2: 3
Work
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8
This works well, the intermediate functions don't do any work when there is nothing subscribed. However, once we resubscribe. We can see that Subscription 2 replays the last state before unsubscribe, and then plays the derived state based on the current value in the base$ state. This is not ideal.
The problem with connected ReplaySubject
=== Hot Observable ===
Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Work
Work
Work
Resubscribe
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8
This one does not have the same problem as the refCounted observable, there is no unnecessary replay of the last state before the unsubscription. However, since the observable is now hot, the tradeoff is that we always do work whenever a new value comes in the base$ state, even though the value is not used by any subscriptions.
Finally, we have the custom operator:
=== Custom Observable ===
Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Resubscribe
Work
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8
Ahh, the best of both worlds. Not only does it not unnecessarily replay the last value before unsubscription, but it also does not unnecessarily do any work when there is no subscription.
This is accomplished by manually creating a combination of RefCount and ReplaySubject. We keep track of each subscriber, and when it hits 0, we flush the replay value. The code for it is here (and in the fiddle, of course):
Rx.Observable.prototype.selectiveReplay = function() {
let subscribers = [];
let innerSubscription;
let storage = null;
return Rx.Observable.create(observer => {
if (subscribers.length > 0) {
observer.next(storage);
}
subscribers.push(observer);
if (!innerSubscription) {
innerSubscription = this.subscribe(val => {
storage = val;
subscribers.forEach(subscriber => subscriber.next(val))
});
}
return () => {
subscribers = subscribers.filter(subscriber => subscriber !== observer);
if (subscribers.length === 0) {
storage = null;
innerSubscription.unsubscribe();
innerSubscription = null;
}
};
});
};
So, this custom observable already works. But, can this be done with only RxJS operators? Keep in mind, potentially there could be more than a couple of these subjects linked together like this. In the example, I'm only using one linking to the base$ to illustrate the issue with both vanilla approaches I've tried at the most basic level.
Basically, if you can use only RxJS operators, and get the output to match the output for === Custom Observable === above. That's what I'm looking for. Thanks!
You should be able to use multicast with a subject factory instead of a subject. Cf. https://jsfiddle.net/pto7ngov/1/
(function(){
console.log('=== RefCounted Observable ===');
var base$ = new Rx.ReplaySubject(1);
var listen$ = base$.map(work).multicast(()=> new Rx.ReplaySubject(1)).refCount();
var subscription1 = listen$.subscribe(x => console.log('Subscription 1: ' + x));
base$.next(1);
base$.next(2);
base$.next(3);
console.log('Unsubscribe');
subscription1.unsubscribe();
base$.next(4);
base$.next(5);
base$.next(6);
console.log('Resubscribe');
var subscription2 = listen$.subscribe(x => console.log('Subscription 2: ' + x));
base$.next(7);
base$.next(8);
})();
This overload of the multicast operator serves exactly your use case. Every time the observable returned by the multicast operator completes and is reconnected to, it creates a new subject using the provided factory. It is not very well documented though, but it basically replicates an existing API from Rxjs v4.
In case I misunderstood or that does not work let me know,

Categories

Resources