Skip to content

Coding practices

Luis Stanley Jovel edited this page Aug 9, 2019 · 2 revisions

Table of contents

Clean up subscriptions

When working with rxjs it is very easy to create memory leaks by not unsubscribing from observables. We usually subscribe to an observable in ngOnInit or ngAferViewInit, when we navigate to some other screen which is implemented using different components the subscription remains alive and still listening for emitted values y we navigate back the subscription is re-created resulting in two of the subscription hence causing a memory leak.

Quote:

Failing to unsubscribe from observables will lead to unwanted memory leaks as the observable stream is left open, potentially even after a component has been destroyed / the user has navigated to another page.

How to properly unsubscribe from observables

There are different approaches when it comes to unsubscribing from observables:

1. Using the async pipe

The async pipe unsubscribes itself automatically and it makes the code simpler by eliminating the need to manually manage subscriptions. It also reduces the risk of accidentally forgetting to unsubscribe a subscription in the component.

 // in the template
<h1>{{ (user$ | async).?username }}</h1>

// in the component
ngOnInit() {
  this.user$ = this.userService.getUser() // we do not subscribe, use the observable directly
}

2. Using .unsubscribe

On the ngOnDestroy life cycle method, call

  ngOnDestroy() {
    this.observable.unsubscribe()
  }

The disadvantage of this approach is that we'll have to remember to imperatively unsubscribe from all observables we use in the component

  ngOnDestroy() {
    this.observable1.unsubscribe()
    .
    .
    .
    this.observable10.unsubscribe()
  }

3. Using take(1) operator

Some subscriptions only have to happen once like api requests. In this scenarios we can use take(1) operator which automatically unsubscribes after receibing the emitted value.

  ngOnInit() {
    this.observable1
      .pipe(take(1))
      .subscribe( value => /* some logic */)
  }

Something to keep in mind with this approach is that if the original observable never emits then take will never fire and it won't unsubscribe.

4. Using takeUntil() operator

This is a more declarative approach that allows declaring our Observable chain beforehand with everything that it needs to accommodate for the whole life cycle from start to end.

TakeUntil emits the values emitted by the source Observable until a notifier Observable emits a value.

Example usage:

class Component implements OnInit, OnDestroy {  
  private unsubscribe$ = new Subject<void>;

  ngOnInit() {
    this.observable1
      .pipe(takeUntil(unsubscribe$))
      .subscribe( value => /* some logic */)
    .
    .
    .
    this.observable10
      .pipe(takeUntil(unsubscribe$))
      .subscribe( value => /* some logic */)
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

The takeUntil() solution is great but unfortunately, it comes also with a couple of disadvantages:

Quote:

Most obviously, it’s quite verbose, we have to create additional Subject and correctly implement OnDestroy interface in every component of our application. An even bigger problem is that it is a quite error-prone process. It is VERY easy to forget to implement OnDestroy interface. Things will NOT result in any obvious errors whatsoever so they are very easy to miss.

sources

Lazy load