DEV Community

Cover image for DRY Way to Manage Subscriptions in Angular Components

DRY Way to Manage Subscriptions in Angular Components

Cover photo by Tim Swaan on Unsplash.

This article explains how to manage subscriptions in Angular components without repeating the same teardown logic in each component.

Common Ways

There are two common ways to manage RxJS subscriptions in Angular components to prevent memory leaks:

Using Subscription

@Component({
  selector: 'interval',
  templateUrl: './interval.component.html',
})
export class IntervalComponent implements OnInit, OnDestroy {
  // initialize `Subscription` object
  private readonly subscriptions = new Subscription();

  ngOnInit(): void {
    // add all subscriptions to it
    this.subscriptions.add(
      interval(1000)
        .pipe(map(i => `== ${i} ==`))
        .subscribe(console.log)
    );

    this.subscriptions.add(
      interval(2000)
        .pipe(map(i => `=== ${i} ===`))
        .subscribe(console.log)
    );
  }

  ngOnDestroy(): void {
    // unsubscribe from all added subscriptions
    // when component is destroyed
    this.subscriptions.unsubscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Destroy Subject

@Component({
  selector: 'interval',
  templateUrl: './interval.component.html',
})
export class IntervalComponent implements OnInit, OnDestroy {
  // initialize destroy subject
  private readonly destroySubject$ = new Subject<void>();

  ngOnInit(): void {
    interval(1000)
      .pipe(
        map(i => `== ${i} ==`),
        // unsubscribe when destroy subject emits an event
        takeUntil(this.destroySubject$)
      )
      .subscribe(console.log);

    interval(2000)
      .pipe(
        map(i => `=== ${i} ===`),
        takeUntil(this.destroySubject$)
      )
      .subscribe(console.log);
  }

  ngOnDestroy(): void {
    // emit destroy event when component is destroyed
    this.destroySubject$.next();
  }
}
Enter fullscreen mode Exit fullscreen mode

Both solutions have the same drawback: We have to initialize the additional property, and add teardown logic to the ngOnDestroy method. However, there is a better way to manage subscriptions in Angular components.

Solution

We can put the teardown logic in a single place by creating Destroy class that extends the Observable class and implements the OnDestroy interface:

@Injectable()
export class Destroy extends Observable<void> implements OnDestroy {
  // initialize destroy subject
  private readonly destroySubject$ = new ReplaySubject<void>(1);

  constructor() {
    // emit destroy event to all subscribers when destroy subject emits
    super(subscriber => this.destroySubject$.subscribe(subscriber));
  }

  ngOnDestroy(): void {
    // emit destroy event when component that injects
    // `Destroy` provider is destroyed
    this.destroySubject$.next();
    this.destroySubject$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, we can provide Destroy at the component level and inject it through the constructor:

@Component({
  // provide `Destroy` at the component level
  viewProviders: [Destroy]
})
export class IntervalComponent implements OnInit {
  // inject it through the constructor
  constructor(private readonly destroy$: Destroy) {}

  ngOnInit(): void {
    interval(1000)
      .pipe(
        map(i => `== ${i} ==`),
        // unsubscribe when `destroy$` Observable emits an event
        takeUntil(this.destroy$)
      )
      .subscribe(console.log);
  }
}
Enter fullscreen mode Exit fullscreen mode

When a provider is provided at the component level, it will be tied to the component lifecycle which allows us to use the ngOnDestroy lifecycle method within it. Therefore, the ngOnDestroy method of the Destroy provider will be called when the IntervalComponent is destroyed.

Conclusion

In general, manual (un)subscriptions in Angular components should be avoided. If you need to perform a side effect at the component level, you can do so using the @ngrx/component-store effects, and let ComponentStore take care to prevent memory leaks. However, if you prefer to manage the side effects in the components, consider using the Destroy provider to avoid repeating the same teardown logic in each component.

Peer Reviewers

Discussion (15)

Collapse
nikosanif profile image
Nikos Anifantis

Great article! 👌
I have written a similar article that describes 5 ways to unsubscribe observables and I think you just proposed the 6th! 🚀

Collapse
madhust profile image
Madhu Sudhanan P • Edited on

Good one Nikos !!

Collapse
nikosanif profile image
Nikos Anifantis

Many many thanks Madhu!

Collapse
markostanimirovic profile image
Marko Stanimirović Author

Thanks Nikos!

Collapse
ashmcconnell profile image
Ash McConnell

I quite like npmjs.com/package/@ngneat/until-de... . I think it's worth a look. I agree though that ComponentStore is fantastic :)

Collapse
2advancesolutions profile image
Batman • Edited on

It’s a good idea to destroy observables using a helper class but not recommend to do this way as it causes each class or component to inherit the helper class to just destroy a subscription. If you expect a observer and want to kill it just Do pipe(take(1)) and if you expect a promise like user profile api request use.toPromise() instead of .subscribe(). I say this from experience using to helper method just cause extra code that’s not need. But great Article

Collapse
ashmcconnell profile image
Ash McConnell

Sorry I skimmed and missed the subtleties of your approach, that's really nice :)

Collapse
markostanimirovic profile image
Marko Stanimirović Author

Thank you :)

Collapse
markostanimirovic profile image
Marko Stanimirović Author

Thanks Ankita! I will :)

Collapse
madhust profile image
Madhu Sudhanan P

Nice one !!

Collapse
slavafomin profile image
Slava Fomin II

Wow, this is clever. This pattern will reduce a lot of boilerplate from my code, thanks!

Collapse
dbrigiita profile image
Dani Brigiita

Thank You Marko

Collapse
ruslangonzalez profile image
Ruslan Gonzalez

Cool! How about the async pipe approach also?

Collapse
markostanimirovic profile image
Marko Stanimirović Author

Yes, async is always a great choice for displaying Observable results on a template :)