05 Sep 2023
3 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!

Frequently asked questions

How do I unsubscribe from an Observable automatically when an Angular component is destroyed?

Use the takeUntilDestroyed() operator from @angular/core/rxjs-interop inside a pipe() before subscribe(). It completes the observable when the component or directive is destroyed, or when its injector is destroyed, so you no longer need to store the Subscription and call unsubscribe() in ngOnDestroy. Called in the constructor or another injection context, it takes no arguments, for example interval(1000).pipe(takeUntilDestroyed()).

Can I use DestroyRef instead of ngOnDestroy in Angular?

You can replace an ngOnDestroy cleanup by registering a callback with DestroyRef.onDestroy(), which runs when the component or directive is destroyed or its injector is destroyed. Obtain the reference with inject(DestroyRef) in the constructor or a field initializer, then call unsubscribe() inside the callback. This avoids storing a Subscription and implementing a lifecycle hook, while still preventing the memory leak caused by an active interval subscription.

Why does inject(DestroyRef) throw an error when called in ngOnInit?

Calling inject(DestroyRef) in ngOnInit throws an error because inject() only works inside an injection context, such as the constructor, class field initializers, or a factory function. Lifecycle hooks like ngOnInit run outside that context. To fix this, inject DestroyRef as a class field, for example #destroyRef = inject(DestroyRef), and pass it to your helper method, or call the helper from the constructor instead.

How do I use takeUntilDestroyed outside the constructor in Angular?

Pass the DestroyRef instance explicitly, for example takeUntilDestroyed(this.#destroyRef). When called outside an injection context, such as in ngOnInit, takeUntilDestroyed() requires you to provide the DestroyRef yourself. Store it as a class field using inject(DestroyRef) during class initialization, then reference that field inside ngOnInit before subscribing to your observable. This keeps the cleanup behavior identical to the constructor version.

What is DestroyRef in Angular and when was it introduced?

DestroyRef is an Angular class, introduced in Angular 16, that lets you register callbacks to run when a component, directive, or its corresponding injector is destroyed. You get an instance with inject(DestroyRef) and register a callback with onDestroy(). A typical use is unsubscribing from an interval(1000) subscription so it does not keep running after a route change destroys the component. It also pairs with takeUntilDestroyed() from @angular/core/rxjs-interop.

Share this post

Sign up for our newsletter

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