How to avoid memory leaks with complex RxJS pipes in Angular 9?

Viewed 3924

I have this little bit complex pipe structure in my Angular + RxJS app:

remote-data.service.ts:

getAll(url): Observable<any[]> {
  return this.http.get(url, this.options)
    .pipe(map((result: any[]) => result));
}

proxy.service.ts:

// localDataService use my LocalDataService
// remoteDataService use my RemoteDataService

getAll(model): Observable<any[]> {
  if (model.use_localstorage) {
    return this.localDataService.getAll(model)
      .pipe(map((result: any[]) => result));
  } else {
    return this.remoteDataService.getAll(model.url)
      .pipe(map((result: any[]) => result));
  }
}

helper.service.ts:

getAll(model): Observable<any[]> {
  // do some general fancy things...
  return this.proxyService.getAll(model)
      .pipe(map((result: any) => result));
  }
}

Then in my component:

export class MyComponent {
  helperService = new HelperService();
  model = new MyModel();

  getAll() {
    this.helperService.getAll().subscribe((result: any[]) => {
      // parse result
    });
  }
}

As you see I build a pipeline from remote data service, proxy service, helper servcice and the component. Of course the reason is to separate each functions from eachother and make my services more reuseable.

My goal is to avoid the memory leaks.

And the question is: if I want to put a take(1) RxJS operator into my pipeline it's enough to put it into the end of pipeline, before the .subscribe() or need to put it every services and component too?

What is the best practice in this case to avoid memory leaks?

2 Answers

It depends. pipe(take(1)) ensures you get a "single Value observable" meaning, that it will emit one value then completes, and closes the subscription, hence no memoryleak. If your Service or function per se emits only a Single value pipe(take(1)) does nothing.

For Example if you have a REST call, with httpclient.get(...) it may have a delay. Then you should use pipe(timeout(3000)) or pipe(takeUntil(...)) to make sure to not have a subscritpion alive, if your component or whatever is destroyed you don't have a memory leak or unexpeced behaviour of the logic inside the subsription. Even with pipe(take(1)) a memoryleak can exist because it will only complete after exactly one value or error is emitted.

So if you have a http.get() and it has a network delay. And you would use "pipe(take(1))" it would still cause a memory leak because it waits for exactly one value, and the subscription will be triggered when the value arrives, even if the Component in wich you made the call is destroyed or you have navigated to an other View of your application.

takeUntil(...) is usefull for components, if they are destroyed you could trigger the ending of a subscription in ngDestroy().

  public isActive = new Subject();

  public ngOnDestroy(): void {
    this.isActive.next(false);
  }

  public fun():void{
    this.fooService.getValue()
                     .pipe(takeUntil(this.isActive))
                     .subscribe( value => console.log(value));
  }

If the Observable/Subject completes there should be no memoryleak after the last value is emitted.

You need to take care of those cases, if you are not sure, there is a "complete()" or if it emits only One value but the latency could be problematic.

The memoryleaks are btw the secondary problem, and not even that big. MOre problematic is, that the logic of your subscribe could be triggered, when you don't want to anymore. Sometimes you want to trigger the logic in the subscription (for example a notification to tell the user something was successful) even though the user already left the view. So it depends.

Also you could store All subscritions in on Single Subscription and unsubscribe if you want to dispose them.

  public subs = new Subscription();

  public ngOnDestroy(): void {
    this.subs.unsubscribe();
  }

  public fun():void{
    const bar: Subscription = this.fooService.getValue()
                     .subscribe( value => console.log(value));
    this.subs.add(bar);
  }

There's a common pattern - takeUntil(this.destroy$) should be the latest operator in any pipe before .subscribe call.

class MyClass {
  destroy$ = new Subject();

  ngOnInit() {
    this.stream$.pipe(
      takeUntil(this.destroy$),
    ).subscribe(data => {
      // some logic.
    });
  }

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

There's a very nice explanation of all issues and how to solve them to avoid memory leaks: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f

Related