TS
tempThermometer = new BehaviorSubject<any>([]);
subscription: Subscription;
const promises = list.map(
(url: any) =>
new Promise(resolve => {
this.subscription = this.global.getData(url.link).pipe(take(1)).subscribe((res) => {
const urlArr = new Array();
urlArr.push(url);
this.tempThermometer.value.filter((data: any) => {
if (data.spinning) {
return data.spinning = urlArr.findIndex((x: any) => x.sensor === data.sensor) === -1
}
return;
});
resolve(res);
}, (err: Error) => {
return reject(err);
});
})
);
merge(...observables).subscribe((results) => {
console.log(results);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
What I want to do here is to unsubscribe the promises, because when I click to other page it still running/fetching a data and I want it to stop when I click to other page.
the unsubscribe doesn't work. how to fix it?
The most basic way is to store the Subscription returned from a call to subscribe, and then calling the unsubscribe method on the Subscription when you leave the page (ngOnDestroy life cycle hook in Angular, more about the lifecycle hooks: here).
In your component:
ngOnInit() {
this.sub = this.something.subscribe( ... )
}
ngOnDestroy() {
this.sub.unsubscribe();
}
There are many other ways too:
Using the async pipe in your template where you need the values. It will unsubscribe automatically for you!
take operator that you used in your example will unsubscribe after N values.
takeWhile operator that will unsubscribe based on a predicate.
Here's an article discussing 6 different ways of unsubscribing: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f
Related
Here is my code in angular
this.service.save(body).subscribe(
resp => {
this.dialog.confirmation({
message: 'save object successfully!'
})
.subscribe((ok) => {
if(ok) {
this.pro.status = resp.status;
this.loadingData(resp);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
forkJoin([s1, s2]).subscribe([r1, r2]) => {
this.view = r1;
this.list = r2;
}
}
});
}
);
So there are many levels of subscribe. Not only it is ugly also the result is wrong and I can't not find it out by debugging. How can I rewrite it with rxjs operators?
You can simplify it using the RxJS operators, like the following:
// import { EMPTY, forkJoin } from 'rxjs';
// import { map, mergeMap } from 'rxjs/operators';
this.service
.save(body)
.pipe(
mergeMap((result) =>
// Merge the main observable with the dialog confirmation one..
// and map it to an object that contains the result from both observables.
this.dialog
.confirmation({ message: 'save object successfully!' })
.pipe(map((confirmed) => ({ result, confirmed })))
),
mergeMap(({ result, confirmed }) => {
if (confirmed) {
this.pro.status = result.status;
this.loadingData(result);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
return forkJoin([s1, s2]);
}
// Don't emit any value, if the dialog is not confirmed:
return EMPTY;
})
)
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
Note: To handle the memory leaks, it's highly recommended to unsubscribe from the observable when you don't need it anymore, and this can be achieved based on your use cases, such as assigning the subscribe function result to a Subscription variable and calling unsubscribe in ngOnDestroy lifecycle hook, or using a Subject with takeUntil operator and calling next/complete functions in ngOnDestroy.
And here is how to use the unsubscribe method for example:
// import { Subscription } from 'rxjs';
#Component({...})
export class AppComponent implements OnInit, OnDestroy {
subscription: Subscription
ngOnInit(): void {
this.subscription = this.service.save(body)
// >>> pipe and other RxJS operators <<<
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}
You can read more about that here: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f
This should be roughly equivalent:
this.service.save(body).pipe(
mergeMap(resp =>
this.dialog.confirmation({
message: 'save object successfully!'
}).pipe(
// This filter acts like your `if(ok)` statement. There's no
// else block, so if it's not okay, then nothing happens. The
// view isn't updated etc.
filter(ok => !!ok),
mapTo(resp)
)
),
tap(resp => {
this.pro.status = resp.status;
// If the following line mutates service/global state,
// it probably won't work as expected
this.loadingData(resp);
}),
mergeMap(_ => forkJoin([
this.service.getSummary(this.id),
this.service.getCost(this.id)
]))
).subscribe([view, list]) => {
this.view = view;
this.list = list;
});
There is an SSE endpoint that shares a subscription if the consumer with the same key is already subscribed. If there is an active subscription the data is being polled from another client.
The problem is that the outer subscription never seems to catch the error and delegate it to the router in order to close the connection with the client: polling stops, but connection stays active.
I think the issue is how I start the subscription that is to be shared... but I can't think of a way to resolve this in another way currently.
Router (SSE) / outer subscription:
...
const clientId = Date.now();
const newClient = {
id: clientId,
res,
};
clients.push(newClient);
const sub = subscriptionService.listenToChanges(req.context, categoryIds).subscribe({
next: (data) => {
if (JSON.stringify(data) !== '{}') {
newClient.res.write(`data: ${JSON.stringify(data)}\n\n`);
} else {
newClient.res.write(': poke...\n\n');
}
},
error: () => {
// we never get here...
next(new InternalError());
clients = clients.filter((c) => c.id !== clientId);
res.end();
},
complete: () => {
res.end();
clients = clients.filter((c) => c.id !== clientId);
},
});
req.on('close', () => {
subscriptionService.stopListening(req.context);
sub.unsubscribe();
clients = clients.filter((c) => c.id !== clientId);
});
...
SubscriptionService
...
#trace()
public listenToChanges(ctx: Context, ids: string[]): Observable<{ [key: string]: Data }> {
const key = ctx.user?.email || ClientTypeKey.Anon;
try {
if (this.pool$[key]) {
return this.pool$[key];
}
this.poolSource[key] = new BehaviorSubject<{ [p: string]: Data }>({});
this.pool$[key] = this.poolSource[key].asObservable();
this.fetchData(ctx, ids);
return this.pool$[key].pipe(
catchError((e) => throwError(e)), // we never get here...
);
} catch (e) {
throw new Error(`Subscription Service Listen returned an error: "${e}"`);
}
}
...
private fetchData(ctx: Context, ids: string[]): void {
const key = ctx.user?.email || ClientTypeKey.Anon;
const sub = this.service.getData(ctx, ids)
.pipe(
catchError((e) => throwError(e)),
).subscribe(
(r) => this.poolSource[key].next(r),
(e) => throwError(e), // last time the error is caught
);
this.subscriptions[key] = sub;
}
...
Polling Service
...
#trace()
public getData(ctx: Context, ids: string[]): Observable<{[key: string]: Data}> {
try {
const key = ctx.user?.email || ClientTypeKey.Anon;
const pollingInterval = config.get('services.pollingInterval') || 10000;
return interval(pollingInterval).pipe(
startWith(0),
switchMap(() => this.getConfig(ctx, !!this.cachedData[key])),
map((r) => this.getUpdatedData(ctx, r.data, ids)),
catchError((e) => throwError(e)),
);
} catch (e) {
throw new Error(`Get Data returned an error: "${e}"`);
}
}
...
throwError doesn't actually throw an error, but rather creates an observable that emits an error.
From the docs:
[throwError] Creates an observable that will create an error instance and push it to the consumer as an error immediately upon subscription.
This is why using it inside subscribe does not work as intended. You should simply throw:
.subscribe(
(r) => this.poolSource[key].next(r),
(e) => throw new Error(e)
);
It seems like you have some unnecessary complexity in the way you are calling fetchData() in order to subscribe and push the result into a BehaviorSubject. I don't know all your requirements, but it seems like maybe you don't need the BehaviorSubject at all.
Instead of subscribing in fetchData(), you could simply return the observable and add that into your pool$ array, or maybe even get rid of fetchData() altogether:
public listenToChanges(ctx: Context, ids: string[]): Observable<{ [key: string]: Data }> {
const key = ctx.user?.email || ClientTypeKey.Anon;
try {
if (!this.pool$[key]) {
this.pool$[key] = this.service.getData(ctx, ids).pipe(
catchError((e) => throwError(e))
);
}
return this.pool$[key];
} catch (e) {
throw new Error(`Subscription Service Listen returned an error: "${e}"`);
}
}
Notes:
with the above simplification, maybe you no longer need the outer try/catch
this isn't a complete solution and may require some tweaks in other places of your code. I just wanted to point out, what seems like unnecessary complexity.
I have a method in the component that calls a service which returns an observable
Component Method code
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
This works fine but when I chain unsubscribe to it, it stops working.
With Unsubscribe - This does not work
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data)).Unsubscribe();
}
Service Code method
convertedFile$: Subject<string> = new Subject<string>();
ToBase64(file: any) {
const myReader = new FileReader();
myReader.onloadend = e => {
this.convertedFile$.next(myReader.result.toString().split(',')[1]);
};
myReader.readAsDataURL(file);
return this.convertedFile$.asObservable();
}
As this a subject, I would like to unsubscribe. How can I do that correctly?
You must declare a Subscription property
First in your component
import { Subscription } from 'rxjs';
Then
fileSubscription: Subscription;
And in your method
public upload(file) {
this.fileSubscription = this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
In ngOnDestroy method
ngOnDestroy() {
if (this.fileSubscription) {
this.fileSubscription.unsubscribe();
}
}
Regards
The method is unsubscribe() not Unsubscribe(). But a more elegant way to get a value of observable and destroy the subscription is use the first operator like that:
import { first } from 'rxjs/operators';
public upload(file) {
this.Service.ToBase64(files[0]).pipe(first())
.subscribe(data => (this.convertedFile = data));
}
I have a method that return a Observable
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
now i subscribe it three time, and after 5 second unsubscribe all:
const sub1 = subFoo(1).subscribe(result => console.log(result));
const sub2 = subFoo(2).subscribe(result => console.log(result));
const sub3 = subFoo(3).subscribe(result => console.log(result));
setTimeout(() => {
sub1.unsubscribe();
sub2.unsubscribe();
sub3.unsubscribe();
}, 5000);
i can handle the complete unsubscrite of all listners?
eg. (in pseudo code):
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
// something like this
observer.onAllListenerAreUnsubscribed(() => {
console.log('All Listener Are Unsubscribed!');
});
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
Live demo: https://stackblitz.com/edit/angular-ayl12r
EDIT July 2022: The same functionality can be achieved since RxJS 7.0 with tap({ subscribe: () => ... }).
An Observable can't know about subscriptions to its chains. If you want to be able to tell how many times someone subscribed you can count it yourself:
let subscriptions = 0;
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
subscriptions++;
...
return (() => {
if (--subscriptions === 0) {
// do whatever...
}
...
})
})
})
You can also collect all subscriptions on the "observer side" into a single Subscription and then add a custom handler when you unsubscribe:
const subs = new Subscription();
subs.add(subFoo(1).subscribe(...));
subs.add(subFoo(2).subscribe(...));
subs.add(subFoo(3).subscribe(...));
subs.add(() => {
// do whatever...
});
subs.unsubscribe(); // Will unsubscribe all subscriptions and then call your custom method.
you can unsubscribe all listeners in one single line so no need to handle that event
subscriptions.add(sub1).add(sub2).add(sub3);
// Then unsubscribe all of them with a single
subscriptions.unsubscribe();
By completing all your observables at once, you are sure you will not get any data leakage.
You can create a subject that will emit once the observables should stop emitting and use the takeUntil() operator on your observables, like so:
const completeSubscription: Subject<void> = new Subject();
const sub1 = subFoo(1)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub2 = subFoo(2)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub3 = subFoo(3)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
setTimeout(() => {
completeSubscription.next();
completeSubscription.complete();
}, 5000);
I'm new with Angular 2 and Observables, but I haven't found a way to "listen" to change on a subscription when it receives a stream, I don't even know if this is possible or if it's the right way.
This is what I used to do with promises:
// Constructor and more code
// ...
ngOnInit(): void {
this.loading.present();
this.getItems()
.then(() => this.loading.dismiss());
}
getItems(): Promise {
return this.itemService
.getItems()
.then(items => this.items = items);
}
refresh(refresher): void {
this.getItems()
.then(() => refresher.complete());
}
I've tried it with subscription/observables but I just don't know how:
// Constructor and more code
// ...
ngOnInit(): void {
this.loading.present();
this.getItems()
.subscribe(() => this.loading.dismiss());
}
getItems(): Subscription {
return this.itemService
.getItems()
.subscribe(items => this.items = items);
}
refresh(refresher): void {
this.getItems()
.subscribe(() => refresher.complete());
}
And of course, I get a compilation error: Property 'subscribe' does not exist on type 'Subscription', any help on how to achieve this with Observables (RxJS)?
A Subscription is a listener, you can't listen to a listener, can you?
Instead, you need to return an Observable to be able to subscribe to. Change your function to be as follow (not tested):
getItems(): Observable<any> {
let obs = this.itemService.getItems().share();
obs.subscribe(items => this.items = items);
return obs;
}