Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ensure super.onDestroy() in derived component

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();
   }
}
like image 340
Liero Avatar asked Mar 04 '19 09:03

Liero


3 Answers

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;
}
like image 77
Marc J. Schmidt Avatar answered Nov 07 '22 20:11

Marc J. Schmidt


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

like image 5
Pradeep Avatar answered Nov 07 '22 19:11

Pradeep


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

like image 3
Jota.Toledo Avatar answered Nov 07 '22 20:11

Jota.Toledo