Make sure to call super.onDestroy() in the child component before overriding it

I find myself with a collection of components that share similar lifecycle logic, so I decided to create a base component that implements the OnDestroy interface.

abstract class BaseComponent implements OnDestroy {
   subscriptions = new Array<Subscription>();
   get model() { return … }

   ngOnDestroy() {
      for (let s of subscriptions) s.unsubscribe();
   }
}

The issue arises when a developer writes a custom onDestroy method in a concrete Component that extends ComponentBase, as there is no clear indication that they need to call super.ngOnDestroy();

Is there a way in TypeScript to provide a warning for this? Or is there another design pattern besides component inheritance? perhaps a unit test could be written to verify ngOnDestroy on all components extending from BaseComponent?

EDIT I have come to realize that the BaseComponent with an array of subscriptions mentioned above is not a good practice and should be avoided. It would be better to use auto-unsubscribing observables.

Take a look at the takeUntil(destroy$) pattern:

class MyComponent implements OnInit, OnDestroy {    
    destroy$ = new Subject();    
  
    constructor(http: HttpService) { }

    ngOnInit() {
        http.get(...).pipe(
          takeUntil(this.destroy$)
        ).subscribe(...);  
    }
    

    ngOnDestroy() {
      destroy$.next();
   }
}

Answer №1

To ensure proper execution, it is recommended to establish a return type for ngOnDestroy that the child component must also adhere to.

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;
    }
}

If the child component fails to return the specified type, it indicates that the method has not been invoked.

export class Child extends Base implements OnDestroy {
    ngOnDestroy(): REQUIRED_SUPER {
    }
}

This results in

TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.

To rectify this issue, the user must follow these guidelines:

ngOnDestroy(): REQUIRED_SUPER {
    return super.ngOnDestroy();
}

or

ngOnDestroy(): REQUIRED_SUPER {
    const superCalled = super.ngOnDestroy();
    //perform additional tasks
    return superCalled;
}

Answer №2

It might be the eleventh hour, but for those who are in need of it!!!

declare abstract class BaseClass implements OnDestroy {

ngOnDestroy(): void { }

constructor() {
    const refOnDestroy = this.ngOnDestroy;
    this.ngOnDestroy = () => {
        refOnDestroy();
       // implement unsubscriptions here
       // e.g. for (let s of subscriptions) s.unsubscribe();
    };
 }
}

For more information, click here

Answer №3

In most object-oriented programming languages, the feature you are searching for is not readily available. Once a method is overridden by a child class, there is no built-in way to ensure that the child class invokes the parent's implementation. In TypeScript, there is an ongoing discussion regarding this functionality in an open issue.

One alternative approach could involve marking the implementation of `ngOnDestroy` in the base class as `final`, and providing base classes with a hook-up method to enable 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 requirements, you may consider having a default NOOP implementation
   // and allowing child classes to override it. This way, you can avoid scattering NOOP 
   // implementations throughout your codebase.
   abstract protected destroyHook(): void;
}


class ChildClass extends BaseComponent {
   protected destroyHook(){//NOOP}
}

Unfortunately, TypeScript does not currently support an equivalent of the `final` keyword at the moment, as discussed in this GitHub issue.

Another noteworthy aspect is that the challenge you are facing stems from how you plan to manage subscriptions on component instances. There are more effective ways to handle this, such as unsubscribing from source observables when the component is destroyed. You can achieve this using something like:

readonly observable$: Observable<string> = ....;
ngOnInit(){
   observable$.pipe(takeUntil(/*this instance is destroyed*/)).subscribe(...)
}

This can be easily accomplished with libraries like this one.

Answer №4

To prevent developers from including a subscribe in components and avoid the need to unsubscribe, consider leveraging reactive programming with rxjs operators and async pipes.

An alternative approach would be to implement a custom class decorator that evaluates all class members to identify instances of subscriptions.

Another option is to utilize a custom rxjs operator that automatically unsubscribes when a component is being destroyed.

While there are several choices available, I recommend utilizing the first method for cleaner code and compatibility with onPush change detection strategy.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How to retain the side menu icon in Ionic 2 even after navigating using navCtrl push

Whenever I navigate to a new page using navCtrl.push, the sidemenu icon (hamburger) disappears and is replaced by a back button instead of coexisting with it. What I am aiming for is to retain the sidemenu icon by positioning it on the right side of the i ...

An issue with the animation script in Ionic

I'm having trouble converting this to an Ionic application. Here's my code sample. Can anyone help me with putting this code correctly in Ionic? I'm trying to achieve something like this example <script src="https://cdnjs.cloudflare.com ...

What is the process for mocking a method from a class that is instantiated within another class using ts mockito in typescript?

I have a specific Class that I want to test using the mocha-chai testing framework in TypeScript. My approach involves incorporating ts-mockito for mocking purposes. export class MainClass implements IMainClass { private mainResource: IMainResource; ...

Using TypeScript to include a custom request header in an Express application

I am attempting to include a unique header in my request, but I need to make adjustments within the interface for this task. The original Request interface makes reference to IncomingHttpHeaders. Therefore, my objective is to expand this interface by intr ...

IDE type inferences are wrong for the Polymorphic React component

declare const MyComponent = <A extends {id: bigint|number}>(props: MyProps<A>) => React.FC<{}> interface MyProps<A extends {id: number}> { data: A[] orderBy: keyof A } declare const x: { id: number, foo: string }[] const F ...

Communicating from JQuery-UI to Angular 6 using callbacks

I incorporated the jQuery-UI MonthPicker into my Angular 6 application, and now I am looking to trigger an Angular action whenever the date changes: ngOnInit() { $(document).ready(function() { $('#myMonthPicker').MonthPicker( ...

What could be causing Typescript Compile Errors to occur during runtime?

In the Visual Studio React + Redux template project, I have created a react component with the following "render()" method: public render() { return ( <React.Fragment> <h1>Welcome to the Adventure Company {th ...

What is the best way to utilize the async pipe along with @defer for efficiently loading and declaring variables in the template within Angular 17

One way I can accomplish this is by using @if. An example of this is: @if(items$ | async; as items), where I can assign the array of items to a variable named 'items' using the 'as' keyword in the template. Is there a similar approach ...

Side navigation in Angular is not causing the main content to shrink

In my layout, I have a container that includes two sidenavs and multiple tables in between them. When I toggle the left sidenav, instead of the expected behavior where the content shrinks to accommodate the sidenav, the tables get pushed to the right as if ...

AADSTS9002326: Token redemption across origins is only allowed for the client type of 'Single-Page Application'. Origin of request: 'capacitor://localhost'

My Ionic application is having trouble authenticating in Azure. I followed the guidance from a stackoverflow thread: Ionic and MSAL Authentication Everything went smoothly except for iOS, where I encountered the following error: AADSTS9002326: Cross ...

I am looking to customize the mask input in my angular 7 application so that it allows either a "-" or a digit as the first character, with all subsequent characters being digits. How can I make this modification?

Within my Angular 7 application, I am working on a method that masks user input. Currently, the method restricts users from inputting anything other than digits. However, I need to modify it so that users can input either a "-" or a digit as the first char ...

Definitions for TypeScript related to the restivus.d.ts file

If you're looking for the TypeScript definition I mentioned, you can find it here. I've been working with a Meteor package called restivus. When using it, you simply instantiate the constructor like this: var Api = new Restivus({ useDefaultA ...

Storing Passport.js Token Locally: Best Practices

Struggling to save the jwt token provided by passport.js in browser localStorage, I am facing challenges with transferring it from the server to the client as it is generated on the server side. If anyone can assist me with setting the server-generated to ...

Is it possible to pass an external function to the RxJs subscribe function?

Upon examining the RxJS subscribe method, I noticed that: subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription; So, I decided to create an example initialization function like this: private ...

How can you create an interface where the value type is determined by the key, but not all keys are predefined?

Below is an illustration of a data structure that I aim to define as a type in TypeScript: const dataExample = { Folder: { "Filename.js": { lines: { total: 100, covered: 80, ...

What is the best way to locate and access a JSON file that is relative to the module I am currently working

I am in the process of creating a package named PackageA, which includes a function called parseJson. This function is designed to accept a file path pointing to a JSON file that needs to be parsed. Now, in another package - PackageB, I would like to invok ...

Enable a fraction of a category

Imagine having a structure like this interface User { name: string; email: string; } along with a function like this updateUser(user: User) { } As currently defined, updateUser cannot accept only a name (updateUser({name: 'Anna'} would fa ...

Understanding the operational aspects of Typescript's target and lib settings

When the tsconfig.json file is configured like this: "target": "es5", "lib": [ "es6", "dom", "es2017" ] It appears that es2017 features are not being converted to es5. For example, code like the following will fail in IE11: var foo = [1, 2, 3].includes( ...

What is the best way to search for and isolate an array object within an array of objects using Javascript?

I am attempting to narrow down the list based on offerings const questions = [ { "id": 2616, "offerings": [{"code": "AA"},{"code": "AB"}]}, { "id": 1505, "offerings": [ ...

What is the best way to avoid passing a value to a component in nextjs?

I'm trying to make this component static and reusable across different pages without passing a parameter when calling it. Is there a way to achieve this while following the official documentation? component: import { GetStaticProps, InferGetServerSid ...