I have bunch of components with very similar lifecycle logic, so I have created my base component, that implements OnDestroy
abstract class BaseComponent implements OnDestroy {
subscriptions = new Array<Subscription>();
get model() { return … }
ngOnDestroy() {
for (let s of subscriptions) s.unsubscribe();
}
}
however, when a developers writes custom onOnDestroy method in concrete Component that extends ComponentBase
, he has no way to know he need to call super.ngOnDestroy()
;
Is there some (typescript) way to ensure warning? Or pattern other that component inheritance?
maybe a unit test that would test ngOnDestroy
on all components that extends BaseComponent
?
EDIT
I came to conslusion, that the BaseComponent
with array of subscriptions above is a common antipattern and should be avoided. Ideally using auto unsubscribing observables.
See this takeUntil(destroy$) pattern:
class MyComponent implements OnInit, OnDestroy {
destroy$ = new Subject();
constructor(http: HttpService) { }
ngOnInit() {
http.get(...).pipe(
takeUntil(this.destroy$)
).subscribe(...);
}
ngOnDestroy() {
onDestroy$.onNext();
}
}
The simplest solution is to define a return type of ngOnDestroy
that the child needs to return as well.
class REQUIRED_SUPER {} //important to not export it, only we should be able to create it.
class Base implements OnDestroy {
ngOnDestroy(): REQUIRED_SUPER {
return new REQUIRED_SUPER;
}
}
so, if your user doesn't return it as well, it means he didn't call your method.
export class Child extends Base implements OnDestroy {
ngOnDestroy(): REQUIRED_SUPER {
}
}
This leads to TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.
To solve it, your user needs to do it like this:
ngOnDestroy(): REQUIRED_SUPER {
return super.ngOnDestroy();
}
or
ngOnDestroy(): REQUIRED_SUPER {
const superCalled = super.ngOnDestroy();
//to stuff
return superCalled;
}
I know its late.. but for someone who needs it.!!!
export abstract class BaseClass implements OnDestroy {
ngOnDestroy(): void { }
constructor() {
const refOnDestroy = this.ngOnDestroy;
this.ngOnDestroy = () => {
refOnDestroy();
// perform unsubscriptions here
// eg: for (let s of subscriptions) s.unsubscribe();
};
}
}
Detailed description can be found here
Most OOP languages dont offer the feature that you are looking for. Once a method is overriden by a child class, there is no way to enforce that the child invokes the parent implementation. In typescript there is an open issue discussing this feature.
A different approach would be to mark the implementation of ngOnDestroy on the base class as final, and give base classes a hook-up method to allow them to delegate tear-down logic. For example:
abstract class BaseComponent implements OnDestroy {
readonly subscriptions = new Array<Subscription>();
get model() { return … }
ngOnDestroy() {
for (let s of subscriptions) s.unsubscribe();
this.destroyHook();
}
// depending on your needs, you might want to have a default NOOP implementation
// and allow child classes to override it. That way you wont need to spread NOOP
// implementations all over your code
abstract protected destroyHook(): void;
}
class ChildClass extends BaseComponent {
protected destroyHook(){//NOOP}
}
Saddly, ts doesnt support an equivalent of the final logic ATM.
Another point of interest is the fact that this issue of yours arises from how you are planing to manage subscriptions on component instances. There are indeed better ways to do this, one of them being taking element from source observables until the component is destroyed. Something like:
readonly observable$: Observable<string> = ....;
ngOnInit(){
observable$.pipe(takeUntil(/*this instance is destroyed*/)).subscribe(...)
}
This can easily be archived with libraries like this
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With