Preventing Memory Leaks in RxJS
Choosing takeUntil
or unsubscribe
over first
or take
Memory leaks can be a major issue when we do not give it adequate importance, when writing reactive applications, we must make sure that the events we listen for are handled in the best way, that we are not listening forever for these events, that their listening stops at some point.
In angular we use RxJS, a library that uses the observer pattern to give us a model of subscriptions and observables to handle reactivity.
it also provides us the ability to manage data flows, control the time, the structure of the data and a long etc that we will not detail in this article, one of the key capabilities in the observer pattern, is the ability to unsubscribe to listen to an event, I do not want to listen forever, I want to listen from a certain time to a certain time
Another key concept but this time introduced by RxJS are the operators, it is something essential in RxJS, they are immutable functions, that is to say, they do not modify the original observable, but they return a new observable, this allows to build observable data flows in a functional and declarative way.
So we could use an operator to complete the execution of an observable and thus stop listening to it ?
Yes and no!
If we use operators we can stop listening to them, but… what if the observable never emits and the operator is not executed ? what if there is a delay for its first emission and then we go to another component it will continue listening ?
The answer is yes, and here we have a performance problem, a memory leak!
let’s see an example in code
Let’s create two components in our angular application, having in total 3 components: appComponent, ChildOneComponent, ChildTwoComponent
ChildOneComponent in ChildOneComponent we will use an operator first to complete the observable and stop listening when it emits the first event
export class ConfigService {
getLang(): Observable<string> {
return of("ES").pipe(delay(10000));
}
}
export class ChildOneComponent implements OnInit {
timer$: Observable<number> = of(0);
constructor(private readonly configService: ConfigService) {}
ngOnInit(): void {
this.timer$ = timer(1000, 1000);
this.configService
.getLang()
.pipe(first())
.subscribe(lang => {
console.log("childOne", { lang });
});
}
ngOnDestroy(): void {
console.log("destroy child one component");
}
}
ChildTwoComponent
in ChildTwoComponent we will use an operator to complete the observable and stop listening when it emits the first event, but we also join one of the capabilities that RxJS gives us to unsubscribe when the component is destroyed, in this way we ensure that when the component is destroyed, we will stop listening.
export class ConfigService {
getLang(): Observable<string> {
return of("ES").pipe(delay(10000));
}
}
export class ChildTwoComponent implements OnInit, OnDestroy {
timer$: Observable<number> = of(0);
constructor(private readonly configService: ConfigService) {}
subscriptions: Subscription = new Subscription();
ngOnInit(): void {
this.timer$ = timer(1000, 1000);
this.subscriptions.add(
this.configService
.getLang()
.pipe(first())
.subscribe(lang => {
console.log("childTwo", { lang });
})
);
}
ngOnDestroy(): void {
console.log("destroy child two component");
this.subscriptions.unsubscribe();
}
}
ChildOneComponent
now we are going to make a small demo, as even using the first operator, that helps us when the first event is emitted to complete the observable, but this does not guarantee us, that we stop listening to that event, causing problems of memory leaks.
We will see that even if the component is destroyed, the observable is still heard.
ChildTwoComponent
on the contrary if we use the capabilities of RxJS to unsubscribe manually, this will guarantee that if the component is destroyed we will cancel the observable stopping the listening, avoiding performance problems.
we will see that when the component is destroyed, the observable will not be emitted anymore, because it has been cancelled.
Tip:
Using the takeUntil operator and theOnDestroy lifecycle we can also create a strategy to unsubscribe from the observable.
export class ChildOneComponent implements OnInit, OnDestroy {
timer$: Observable<number> = of(0);
destroy$: Subject<void> = new Subject();
constructor(private readonly configService: ConfigService) {}
ngOnInit(): void {
this.timer$ = timer(1000, 1000);
this.configService
.getLang()
.pipe(takeUntil(this.destroy$), first())
.subscribe(lang => {
console.log("childOne", { lang });
});
}
ngOnDestroy(): void {
console.log("destroy child one component");
this.destroy$.next();
this.destroy$.complete();
}
}