05 Sep 2023
5 min

Get To Know The DestroyRef!

DestroyRef has been introduced in Angular 16 (commit link). It gives us the option to run a callback function when the component/directive is destroyed or when the corresponding injector is destroyed.

Let’s see an easy example to understand how we can use that.

Callback when a component is being destroyed

import { Component } from '@angular/core';
import { interval } from 'rxjs';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: ``,
})
export default class DashboardComponent {
  constructor() {
    interval(1000).subscribe((value) => {
      console.log(value);
    });
  }
}

The code above emits a new value every 1 sec (1000ms) and logs a value to the console. It’s a small piece of code, but it still creates a memory leak since we are not destroying the subscription.

Let’s answer some questions you may have.

Q: What would happen if we changed the route?

A: Well, the component would be destroyed.

Q: What would happen if we came back to this route?

A: Well, the component would be constructed again.

Despite the component being destroyed, the subscription remains active.

import { Component, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: ``,
})
export default class DashboardComponent implements OnDestroy {
  #subscription?: Subscription;
  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });
  }
  
  ngOnDestroy(): void {
    this.#subscription?.unsubscribe();
  }
}

We have to unsubscribe the subscription to avoid creating a memory leak. But perhaps you are already doing this ​?

Let’s do the same, but this time using `DestroyRef`

import { Component, DestroyRef, inject } from '@angular/core';
import { Subscription, interval } from 'rxjs';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: ``,
})
export default class DashboardComponent {
  #subscription?: Subscription;
  #destroyRef = inject(DestroyRef);

  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });

    this.#destroyRef.onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }
}

Let’s read the code from top to bottom.

  • ​We are creating a #destroyRef instance using the inject method. Please note that this is happening during the injection context.
  • We are registering a callback function in the onDestroy method. The given function will be executed when the component is being destroyed.

Alternatively, we could write the same piece of code like that:

export default class DashboardComponent {
  #subscription?: Subscription;

  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });

    inject(DestroyRef).onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }
}

Note: This time, we are using the inject function in the constructor. This still works fine since we are in the injection context.

There is a better way to unsubscribe, though. Keep reading 🙂

TakeUntilDestroyed

Before we look at a better way to unsubscribe, let’s dig into some important details.

export default class DashboardComponent {
  #subscription?: Subscription;

  myTakeUntilDestroyed() {
    inject(DestroyRef).onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }

  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });

    this.myTakeUntilDestroyed();
  }
}

I have created the method `myTakeUntilDestroyed`, which injects `DestroyRef`.

It’s important to understand that we cannot use the inject method outside the injection context. 

In the example above, I call `myTakeUntilDestroyed` from the constructor, which works fine.

Injection Context: Constructor, class fields, factory method. Read more

What would happen if we call the method from the `ngOnInit` hook?

export default class DashboardComponent implements OnInit {
  #subscription?: Subscription;

  myTakeUntilDestroyed() {
    inject(DestroyRef).onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }

  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });
  }

  ngOnInit(): void {
    this.myTakeUntilDestroyed();
  }
}

Since we are not in the injection context, Angular will throw an error.

If we, however, have to call `myTakeUntilDestroyed` from the `ngOnInit` hook, we should change how we access `DestroyRef`.

myTakeUntilDestroyed(destroyRef?: DestroyRef) {
    (destroyRef ?? inject(DestroyRef)).onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }

This change allows the developer to use `myTakeUntilDestroyed` outside of the injection context. As such, the code will become:

export default class DashboardComponent implements OnInit {
  #subscription?: Subscription;
  #destroyRef = inject(DestroyRef);

  myTakeUntilDestroyed(destroyRef?: DestroyRef) {
    (destroyRef ?? inject(DestroyRef)).onDestroy(() => {
      this.#subscription?.unsubscribe();
    });
  }

  constructor() {
    this.#subscription = interval(1000).subscribe((value) => {
      console.log(value);
    });
  }

  ngOnInit(): void {
    this.myTakeUntilDestroyed(this.#destroyRef);
  }
}

So far, we have covered some important details, and we are now ready to start using the `takeUntilDestroyed` rxjs operator.

takeUntilDestroyed completes the observable when the component/directive is destroyed or when the corresponding injector is destroyed!

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export default class DashboardComponent {
  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe((value) => {
        console.log(value);
      });
  }
}

That’s great! We have achieved the same with less and easy-to-read code. Nice!

Oh, wait, how about the `ngOnInit` hook?

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export default class DashboardComponent implements OnInit {
  #destroyRef = inject(DestroyRef);

  ngOnInit(): void {
    interval(1000)
      .pipe(takeUntilDestroyed(this.#destroyRef))
      .subscribe((value) => {
        console.log(value);
      });
  }
}

If we have to use the `takeUntilDestroyed` operator outside the injection context, we (the developers) are responsible for providing `DestroyRef`, similar as in our custom myTakeUntilDestroyed method.

If you enjoy watching videos, you must take a look at this one that covers the same content as the article 

Get To Know the Angular DestroyRef

Useful links:

Thanks for reading my article!

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.