3 Patterns to Prevent Memory Leaks in Angular Components for Observables
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.
Love Web Development?
Angular, Google Cloud, C#, Node, NestJs?
Goals
- 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.
Thanks for your interest in Nx GCP Starter. We'll be in touch shortly
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