Better way to unsubscribe in Angular
You might be wondering why do we need to unsubscribe every subscriptions. Well, you can get the answer from our friend here Netanel Basal. He have a nice .gif
example so check it out.
As time goes on, we(developer) really tired of keep repeating the same process:
- Importing ngOnDestroy,
- Implement the interface
- Create public function ngOnDestroy() { … }
- Create a variables to keep list of subscriptions
- …
- …
- And repeat the whole process for other components.
This is tedious and a waste productivity.
So, I’m going to share a 2 approach on how to unsubscribe subscription easily. But let’s start with normal approach.
- Using Inheritance
<script type=”application/javascript” src=”https://gist.github.com/AzrizHaziq/837befe71d64d5e73ae540e981ded24a.js?file=ANYCHILD.ts”></script>
Create a BaseComponent so that, whoever extends this component are able to easily add subscription and easily unsubscribe on destroy.
// base-inheritence.ts
import { OnDestroy } from "@angular/core";
import { takeUntil, take } from "rxjs/operators";
import { interval, Observable, Subject } from "rxjs";
export class BaseComponent implements OnDestroy {
private isAlive$ = new Subject<any>();
protected unsubsribeOnDestroy = <T = unknown>(source: Observable<T>): Observable<T> =>
source.pipe(takeUntil(this.isAlive$));
// automatic close the subscribtion.
protected once = <T = unknown>(source: Observable<T>): Observable<T> =>
source.pipe(take(1));
/**
* Auto-unsubscribe all subscriptions
* if a component that extend this class, then it should call super.ngOnDestroy()
*/
public ngOnDestroy() {
this.isAlive$.next();
this.isAlive$.complete();
}
}
And this is how to implement in component class.
import { interval, merge } from "rxjs";
import { Component, OnInit, OnDestroy } from "@angular/core";
// inheritence.component.ts
@Component({
selector: "inheritance",
template: "Inheritance: see in logs"
})
export class InheritanceComponent extends BaseComponent implements OnInit, OnDestroy {
// if you have constructor don't forget to call super().
constructor() {
super();
}
ngOnInit() {
interval(1000)
.pipe(
this.unsubsribeOnDestroy,
tap(e => console.log("I:unsubsribeOnDestroy", e))
)
.subscribe();
interval(3000)
.pipe(
tap(e => console.log("I:once", e)),
this.once
)
.subscribe();
}
// implement like this if a component have ngOnDestroy()
ngOnDestroy() {
super.ngOnDestroy();
}
}}
One caveat tho, if the component extends BaseComponent and it also have implment ngOnDestroy() you would have to add super.ngOnDestroy();
...public ngOnDestroy() {
... // things that you need to clean up super.ngOnDestroy(); // don't forget to call this line.
}
2. Decorators
I think this approach kinda complicated a little bit as it mutate prototype.
// Decorators
import { Observable, Subject } from "rxjs";
import { takeUntil, take } from "rxjs/operators";
export function UnsubscribeDecorator(ComponentClass) {
let destroyed$ = new Subject();
const unsubsribeOnDestroy = <T = unknown>(
source: Observable<T>
): Observable<T> => {
destroyed$ = new Subject();
return source.pipe(takeUntil(destroyed$));
};
const once = <T = unknown>(source: Observable<T>): Observable<T> => {
return source.pipe(take(1));
};
const ngOnDestroy = (): void => {
destroyed$.next(true);
destroyed$.complete();
};
Object.assign(ComponentClass.prototype, {
destroyed$,
unsubsribeOnDestroy,
once,
// if target class already have ngOnDestroy implemented, then that class must explicitly call this.killSub$()
...(!!ComponentClass.prototype.ngOnDestroy
? { killSub$: ngOnDestroy }
: { ngOnDestroy })
});
return ComponentClass;
}
And here how to implement it
import { interval, merge } from "rxjs";
import { Component, OnInit, OnDestroy } from "@angular/core";
// Decorator Component
@UnsubscribeDecorator
@Component({
selector: "decorator",
template: "Decorator: see in logs"
})
export class DecoratorComponent implements OnInit, OnDestroy {
ngOnInit() {
interval(1000)
.pipe(
this.unsubsribeOnDestroy, // <--- typescript error
tap(e => console.log("D:unsubsribeOnDestroy", e))
)
.subscribe();
merge(
interval(3000).pipe(
tap(e => console.log("D:once", e)),
this.once // <--- typescript error
),
interval(5000).pipe(
tap(e => console.log("D:once:2", e)),
this.once // <--- typescript error
)
).subscribe();
}
// implement like this if a component have ngOnDestroy()
ngOnDestroy() {
this.killSub$(); // <---typescript error
}
}
Thats all, feel free to play around here.
Update
- Decorator approach with previous example it doesn’t really work
- Added
once
method to auto close the subscription. Can be really help full for something like click button, then trigger one http, and after that close it. (save your time to importtake
) - Added type to functions params and return
- I’m still having difficulty with the return type in Decorator approach. Any typescripts master, please help me with this.