DRY Way to Manage Subscriptions in Angular Components

Marko Stanimirović - Sep 28 '21 - - Dev Community

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .