Angular 4+ ngOnDestroy() in service - destroy observable
Asked Answered
D

8

144

In an angular application we have ngOnDestroy() lifecycle hook for a component / directive and we use this hook to unsubscribe the observables.

I want to clear / destory observable that are created in an @injectable() service. I saw some posts saying that ngOnDestroy() can be used in a service as well.

But, is it a good practice and only way to do so and When will it get called ? someone please clarify.

Digitalize answered 26/8, 2017 at 19:27 Comment(0)
C
172

OnDestroy lifecycle hook is available in providers. According to the docs:

Lifecycle hook that is called when a directive, pipe or service is destroyed.

Here's an example:

@Injectable()
class Service implements OnDestroy {
  ngOnDestroy() {
    console.log('Service destroy')
  }
}

@Component({
  selector: 'foo',
  template: `foo`,
  providers: [Service]
})
export class Foo implements OnDestroy {
  constructor(service: Service) {}

  ngOnDestroy() {
    console.log('foo destroy')
  }
}

@Component({
  selector: 'my-app',
  template: `<foo *ngIf="isFoo"></foo>`,
})
export class App {
  isFoo = true;

  constructor() {
    setTimeout(() => {
        this.isFoo = false;
    }, 1000)
  }
}

Notice that in the code above Service is an instance that belongs to Foo component, so it can be destroyed when Foo is destroyed.

For providers that belong to root injector this will happen on application destroy, this is helpful to avoid memory leaks with multiple bootstraps, i.e. in tests.

When a provider from parent injector is subscribed in child component, it won't be destroyed on component destroy, this is component's responsibility to unsubscribe in component ngOnDestroy (as another answer explains).

Capstone answered 27/8, 2017 at 10:31 Comment(8)
No class Service implements OnDestroy ? And what do you think when this is called if service is provided on module levelProctor
implements OnDestroy doesn't affect anything but can be added for completeness. It will be called when a module is destroyed, like appModule.destroy(). This may be useful for multiple app initializations.Capstone
is unsubscribe necessary for every component that uses services?Adrieneadrienne
@AliAbbaszade May depend on what it unsubscribes from. But likely yes, otherwise this will result in memory leaks.Capstone
The Plunker wasn't working for me, so here's a StackBlitz version of the example: stackblitz.com/edit/angular-mggk9bErrol
I think you are missing implements OnDestroy in the declaration of your component (Foo)Guido
I had some trouble to understand this. But this discussion helped me understand the difference between local and global services: #50056946 Whether you have to "clean up" or not depends from the scope of your service, I think.Ibson
If some want to vote to fix the module not being destructed, you can do so here github.com/angular/angular/issues/37095#issuecomment-854792361Excited
N
31

Create a variable in your service

subscriptions: Subscriptions[]=[];

Push each of your subscribe to the array as

this.subscriptions.push(...)

Write a dispose() method

dispose(){
this.subscriptions.forEach(subscription =>subscription.unsubscribe())

Call this method from your component during ngOnDestroy

ngOnDestroy(){
   this.service.dispose();
 }
Nyhagen answered 26/8, 2017 at 19:32 Comment(13)
Thank you for your reply. Do we have any idea when this ngOnDestroy will be called. ?Digitalize
yes it says its a cleanup call before the directive or component gets destroyed. but i just want to understand whether is it applicable for service as well ?Digitalize
No services will be cleared when the module is unloadedNyhagen
life cycle hooks are not applicable for @injectablesNyhagen
@Nyhagen I'm not sure when they were introduced but they are.Capstone
@estus can you elaborate. the commentNyhagen
Lifecycle hooks are applicable to providers as well. Since v4, I guess.Capstone
@estus I tried life cycle hooks at service it is not working in the 4.x versions tooNyhagen
I've posted an answer that explains this.Capstone
@Nyhagen You can also just create one Subscription() object ad hoc and add to it. So you end up with _subscriptions = new Subscription(); then this._subscriptions.add(.....) and in ngOnDestroy() you just unsubscribe and don't need the array logic this._subscription.unsubscribe().Listing
It's something new to me I'll check this out and see. Thanks mateNyhagen
A lifecycle hook that is called when a directive, pipe, or service is destroyed. Use for any custom cleanup that needs to occur when the instance is destroyed.Jaejaeger
No. This way the component becomes responsible of disposal of a service, which it did not create. What if 2 different components use the service? Dependency injection framework creates the service, so dependency injection framework must control the service lifetime, not some component.Sweeper
G
18

I prefer this takeUntil(onDestroy$) pattern enabled by pipable operators. I like that this pattern is more concise, more clean, and it clearly conveys the intent to kill a subscription upon execution of the OnDestroy lifecycle hook.

This pattern works for services as well as components subscribing to injected observables. The skeleton code below should give you enough detail to integrate the pattern into your own service. Imagine you're importing a service called InjectedService...

import { InjectedService } from 'where/it/lives';
import { Injectable, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class MyService implements OnDestroy {

  private onDestroy$ = new Subject<boolean>();

  constructor(
    private injectedService: InjectedService
  ) {
    // Subscribe to service, and automatically unsubscribe upon `ngOnDestroy`
    this.injectedService.observableThing().pipe(
      takeUntil(this.onDestroy$)
    ).subscribe(latestTask => {
      if (latestTask) {
        this.initializeDraftAllocations();
      }
    });
  }

  ngOnDestroy() {
    this.onDestroy$.next(true);
    this.onDestroy$.complete();
  }

The topic of when/how to unsubscribe is covered extensively here: Angular/RxJs When should I unsubscribe from `Subscription`

Gliwice answered 8/9, 2018 at 5:41 Comment(0)
K
5

Just to clarify - you don't need to destroy Observables but only the subscriptions made to them.

It seems like others have pointed out that you are now able to use ngOnDestroy with services as well. Link: https://angular.io/api/core/OnDestroy

Kr answered 26/8, 2017 at 21:56 Comment(1)
Can you please elaborate more on itNyhagen
L
4

Caution if using tokens

In trying to make my application as modular as possible I'll often use provider tokens to provide a service to a component. It seems that these do NOT get their ngOnDestroy methods called :-(

eg.

export const PAYMENTPANEL_SERVICE = new InjectionToken<PaymentPanelService>('PAYMENTPANEL_SERVICE');

With a provider section in a component:

 {
     provide: PAYMENTPANEL_SERVICE,
     useExisting: ShopPaymentPanelService
 }

My ShopPaymentPanelService does NOT have its ngOnDestroy method called when the component is disposed. I just found this out the hard way!

A workaround is to provide the service in conjunction with useExisting.

[
   ShopPaymentPanelService,

   {
       provide: PAYMENTPANEL_SERVICE,
       useExisting: ShopPaymentPanelService
   }
]

When I did this the ngOnDispose was called as expected.

Not sure if this is a bug or not but very unexpected.

Listing answered 29/8, 2019 at 1:0 Comment(1)
Great hint! I was wondering why it wasn't working in my case (I was using abstract class-interface as a token for concrete implementation).Moral
N
1

Create a variable in your service:

private subscriptions$ = new Subscription();

Add each of your subscriptions to observable in constructor (or in ngOnInit lifecycle hook)

ngOnInit() {
  this.subscriptions.add(...)
  this.subscriptions.add(...)
}

Call this method from your component on destroy to unsubscribe from all subscriptions and child subscriptions.

ngOnDestroy(){
   this.subsriptions$.unsubscribe();
}
Newness answered 7/6, 2021 at 17:7 Comment(1)
I think $ prefix is usually used for observables but no for subscriptions. The convention I follow: rat is a Rat object, rats is Rat[] (or List<Rat>) and rat$ is Observable<Rat>. Anyway, IMHO this is best answer.Aerology
S
0

I recommend using .pipe(take(1)).subscribe(). To avoid setting up an ongoing subscription.

Scevor answered 10/2, 2022 at 12:2 Comment(0)
R
0

TLDR;

import {DestroyRef} from "@angular/core";

// ...

constructor(destroyRef: DestroyRef) {
  fromEvent(window, 'resize')
    .pipe(takeUntilDestroyed(destroyRef))
    .subscribe(console.warn);
}

The Explanation

Inject the DestroyRef into your target stereotype (module, service, directive, component, pipe), then use RxJS takeUntilDestroyed operator.

This is because Angular's dependency injection (DI) & inversion of control (IoC) are hierarchal. Thus, each stereotype has its own injector, usually inheriting from a parent injector.

And so, each injector manager its own lifecycle and exposes a DestroyRef.

Roscoeroscommon answered 3/6, 2023 at 13:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.