After numerous code reviews I've perused in my career, the most misunderstood concept facing developers surfaces around RxJs and Observables in components.  Careless subscriptions left open after destroy and removal from the DOM could cause memory leaks.

I'll explain three patterns to use for unsubscribing from observables in components.

I'm susceptible to RxJs esoteric patterns and operators too, check out My Three Pitfalls Using RxJs in NgRx Effects.

Goals

  1. Describe Three Patterns of Unsubscribing or Completing the Stream

First, it needs to be understood that subscriptions essentially create a callback.  As a component subscribes to data that pushed within the observable, the call back keeps listening regardless of the component no longer existing in the DOM.  A reference to the call back still exists.  Therefore, the observable either needs to be unsubscribed, or the observable needs to be marked as completed.

Pattern #1 Exclusive Unsubscribe

As developers need to retrieve information from different observables within the components, the I've found a property private mySubscription: ISubscription with an assignment to an observable in the ngOnInit life cycle hook of the component.

The problem with the above code is the subscription is left open when the component removes from the DOM.

Now imagine we have multiple observable data sources, one property for each subscription and exclusively unsubscribing from each.  x number of subscriptions to check if they exist and unsubscribe would be tedious.  In this case, completing the stream would be optimal with a common trigger when the component is torn down, and that will be covered in pattern #3.

Free Your Developers

Nx Monorepo Starter alleviates developers from re-inventing deployments on popular Google Cloud Services, standardizes on libraries, and saves time for scaffolding projects.

View on Gumroad

Pattern #2 - async pipe

Fortunately, a pipe exists that reads an observable and changes based upon when values emit through the stream.  The async pipe taps into a couple of things.  One, the async pipe subscribes to the observable provided.  Second, it implements the ngOnDestroy lifecycle hook then unsubscribes.

I prefer this method for reading observables in my components.  It involves less cognitive load in the typescript code for your component, as seen above in explicitly unsubscribing and later using an RxJs operator takeUntil.  Less code is written using this practice.

Using the title$ property in the template, I add this to the template with the async pipe without worrying about exclusive unsubscribing.

This could pose it's own set of challenges with an object more complex than a string, and the properties of that object need spread throughout the template.  The async pipe would be littered throughout the template.  In the case, I recommend using this package in project @ngrx-utils/store.  The NgLetModule contains a directive *ngLet to resolve this issue.

As you can imagine, the customer$ properties will be used in a number of different places in the template.  Using the *ngLet module resolves this issue keeping your template cleaner.

Pattern #3 takeUntil / ngOnDestroy

Using the async pipe mentioned in practice #2 may not be feasible per the use case and creating an explicit subscription might be the only option.  Keep in mind, the preventing memory leaks the stream needs to complete or explicitly unsubscribing needs to happen.  In this case, the takeUntil operator will complete the stream.

The takeUntil operator completes the stream when it's inner stream emits a value.  When subscribing to an observable, the stream needs to complete via the ngOnDestroy lifecycle hook.

In the component, create a property private destroy$ = new Subject<void>.  This property will be used to emit a value in the ngOnDestroy lifecycle hook and used in conjunction in the takeUntil operators for each observable needing a subscription.

In the following use case, I want to redirect the user on a successful login from an action dispatched from NgRx.

On a successful login, the page redirects via the subscription on a successful login for the user.  This page redirect tears down the take-until.component.ts and calls the ngOnDestroy lifecycle hook.  Within the life cycle hook, a value is emitted from the this.destroy$.next().  The takeUntil operator in the subscription will complete the stream as the inner observable this.destroy$ emits a value.

This pattern works well if multiple subscriptions need to be completed when the component destroys.  There's no explicit unsubscribe or adding more properties to handle or even check if that subscription property for being is defined.

This implementation can be tedious to enter in every component with subscriptions.  To keep the code cleaner, the @ngneat/until-destroy package provides an decorator so adding the this.destroy$ subject and ngOnDestroy lifecycle hook to each component.

Conclusion

Preventing memory leaks per the app creates a better user experience.  The three patterns described above help complete streams once a component removes from the DOM.

I list my order of preference to help prevent memory leaks.  I'm not listing pattern #1 as every instance of a component I've used will not use this pattern.  It's too much overhead.

Pattern #2 - Use the Async Pipe

Pattern #3 - takeUntil / ngOnDestroy