'Subscribe fires twice when I revisit a page
I'm running into a scenerio where subscribe fires 2x. However if I get rid of the service by copy pasting the poller method into my component then the issue goes away.
I am running in Ionic 3, but I don't think that matters here. "rxjs": "5.4.3"
unsubscribe() is called on ngDestroy()
my-service.service.ts
public pollingDataReceived = new Subject<any>();
pollForData = (id: string) : Subscription => {
let stopPollingForStatus = false;
return Observable.interval(environment['pollingInterval'])
.takeWhile(() => !stopPollingForStatus)
.concatMap(() => {
return this.getAnalysisById(id);
})
.subscribe((analysisData) => {
if (analysisData) {
if (analysisData.status === 'questions') {
stopPollingForStatus = false;
}
else {
stopPollingForStatus = true;
const result = {
someData: 'whatever'
};
console.log('raise pollingDataReceived event');// This happens ONCE
this.pollingDataReceived.next(result);
}
}
else {
alert('no analysis data!');
}
});
}
my.component.ts (a subscriber)
private pollingData: Subscription;
someEventHandler() {
this.pollingData = this.pollForData(this.id);
}
ngOnInit(): void {
this.myService.pollingDataReceived.subscribe((data) => {
this.analysis = data.analysisData;
//This is getting called 2x. Maybe I double subscribed?
myNavigator.navigateToPageByStatus(myPage);
}, (err) => { console.log(err); });
}
ngOnDestroy() {
console.log('ngOnDestroy: unsubscribe pollingData in flow');// This does get called
this.pollingData.unsubscribe();
}
Solution 1:[1]
Angular Services behave like Singletons unless provided at a component level. Hence the pollingDataReceived Subject will continue to hold its subscriptions until the service is in scope.
In your case, the stale subscription to this.myService.pollingDataReceived from from the earlier visit also is getting fired. Hence the subscriber firing twice on revisit.
Cleaning it up on ngDestroy similar to pollingData will fix the issue.
Solution 2:[2]
The reason for handler firing twice — is that you never unsubscribe from the service pollingDataReceived Subject, while you push to this subject multiple times. Your service was likely created once, while your components were recreated multiple times with page revisits. Components were destroyed, though their subscriptions to the same Subject remained intact.
Simplest way to workaround that is to unsubscribe from this subject in component ngOnDestroy method, as @ashish.gd suggested.
Creating new Subjects each time (as suggested here) will probably lead to memory leaks. As your old subscriptions will keep listening to old Subjects. Beware.
While the simplest way would be to unsubscribe from pollingDataReceived Subject, the better way would be to have your service pollForData() to return an Observable.
Unless you need several consumers to read the same pollingDataReceived Subject — you don't need to have a subscription there. Heres a sample code to reflect that idea:
pollForData = (id: string) : Observable => {
return Observable.interval(environment['pollingInterval'])
.concatMap(() => this.getAnalysisById(id))
.takeWhile(data => data.status === 'questions')
// other handling logic goes here
// .map(), .switchMap(), etc
// NOTE: no subscription here
}
and in the controller you could do something like
// just an utility subject to track component destruction
onDestroy$ = new Subject();
someEventHandler() {
this.pollForData(this.id).pipe(
// this will ensure that subscription will be completed on component destruction
takeUntil(this.onDestroy$)
).subscribe((data) => {
this.analysis = data.analysisData;
myNavigator.navigateToPageByStatus(myPage);
}, (err) => { console.log(err); });
}
ngOnDestroy() {
this.onDestroy$.next(void 0);
}
--
NOTE 1: seems like currently your service is provided at module level, which means that if you'll ever have two consumers requesting pollForData(id) at the same time (e.g. two components on one page) — both of these requests will "write" into the same Subject. Error-prone behavior.
NOTE 2: currently in your code takeWhile predicate wont be run until you get a new event on the stream
Observable.interval(environment['pollingInterval'])
.takeWhile(() => !stopPollingForStatus)
Which means that you'll have an unnecessary subscription in memory for environment['pollingInterval'] time. This should not be critical, yet it might be troublesome depending on timing.
--
Hope this helps, happy coding!
Solution 3:[3]
Use a finalise subject and take until
private pollingData: Subscription;
private finalise = new Subject<void>();
someEventHandler() {
this.pollingData = this.pollForData(this.id);
}
ngOnInit(): void {
this.myService.pollingDataReceived.pipe(
takeUntil(finalise)
).subscribe((data) => {
this.analysis = data.analysisData;
//This is getting called 2x. Maybe I double subscribed?
myNavigator.navigateToPageByStatus(myPage);
}, (err) => { console.log(err); });
}
ngOnDestroy() {
this.finalise.next();
this.finalise.complete();
}
Solution 4:[4]
There is also another way to achieve this. You can use the ReplaySubject from rxjs.
class pollingComponent {
private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);
constructor(
private pollingService: Service) {}
ngOnInit() {
this.pollingService
.takeUntil(this.destroyed$)
.subscribe(...);
}
ngOnDestroy() {
this.destroyed$.next(true);
this.destroyed$.complete();
}
}
Basically it is advised to avoid vanilla Subject. You can read more about the difference here
Solution 5:[5]
If you need the event to stay subscribed while in you are visiting other components (like for messaging) you can subscribe only one time by setting a condition to check if there are no observers to the event and only then subscribe to it. I believe this is not best practice but it does the job.
It would look like this:
ngOnInit(): void {
if (this.myService.pollingDataReceived.observers.length === 0) {
this.myService.pollingDataReceived.subscribe((data) => {
this.analysis = data.analysisData;
myNavigator.navigateToPageByStatus(myPage);
}, (err) => { console.log(err); });
}
}
Solution 6:[6]
The selected answer didn't work for me instead it triggered subscribe once again.
Unsubscribing BehaviorSubject is correct choice.
ngOnDestroy() {
this.pollingData.unsubscribe();
}
BehaviorSubject unsubscribe event triggers property updates but won't get reinitialized. Instead use closed property.
Before initializing again wrap it with below condition and then call next
if(this.pollingData.closed) {
this.pollingData = new BehaviorSubject<any>(false);
}
this.pollingData.next(res);
This worked perfectly and prevents triggering multiple subscribe events. To know about properties check out this answer
If Subject gets completed:
closed: false
isStopped: true
If unsubscribed:
closed: true
isStopped: true
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | ashish.gd |
| Solution 2 | |
| Solution 3 | Adrian Brand |
| Solution 4 | argo |
| Solution 5 | Adi Hershkovitz |
| Solution 6 | Praveen |
