Search code examples
angularrxjsunsubscribe

Is it necessary to unsubscribe/complete in a backend service to prevent side effects?


There has been a lot discussion about when and how to unsubscribe and that subscriptions shouldn't leak outside of a service. I have read many articles about this matter and I think I understood some parts, but what I'm missing in the writeups and tutorials if found, is what to do with the "new subscription pattern" we created.

So I will first explain from which concept I'm talking and put the question afterwards.


CONCEPT:

// some.component.ts (extremely simplified)

this.someSub = this.backendService.someStuff.subscribe(...);

ngOnDestroy() {
  this.someSub.unsubscribe(); // I hope we agree that it is best practice to unsubscribe here, but anyway this is not the question
}

// backend.service.ts

export class BackendService {
  private _someStuff = new BehaviorSubject<any>(null);
  readonly someStuff = this._someStuff.asObservable();

  constructor(http: HttpClient) { }

  getStuff() {
    this.http.get('...').subscribe( response => this._someStuff.next(response) );

  }

QUESTION:

The above is what I found a good solution from my investigations to not leak the HTTP subscriptions out of the service, but there was also much discussion about the fact that you should unsubscribe form HTTP observables, even though they complete themselves. So I assume this is true for services as well. But I never saw that the HTTP requests in services were unsubscribed nor were the subjects completed. Since I expect this is necessary, the following shows the backenService how I would expect it should be:

// backend.service.ts

export class BackendService implements OnDestroy {                      // A. //
  httpSubs = new Subscription();                                        // B1. //

  private _someStuff = new BehaviorSubject<any>(null);
  readonly someStuff = this._someStuff.asObservable();

  constructor(http: HttpClient) { }

  getStuff() {
    this.httpSubs.add(this.http.get('...')                              // B2. //
      .subscribe( response => this._someStuff.next(response) )
    );     
  }

  // rest of CRUD implementations
  // ...

  ngOnDestroy(): void {                                                               
    this.httpSubs.unsubscribe();                                        // B3. //
    this._someStuff.complete();                                         // C. //
  }

I marked things (A.-C.) that are not as I have seen in common tutorials and stuff.
If I don't miss something fundamental about components,services and how missing unsubscriptions might cause side effects... then my questions regarding if unsubscirbe/complete in a service is necessary are:

  1. I expect now that we have the subscription in the service, it is mandatory to implement OnDestroy here (A.), as we do in the components to be able to unsubscribe correctly. I guess this is even the case when the service is provided in root (and therfore will destroyed as one of the last parts of the application), since it might be possible that the service causes side effects on another root service that also isn't garbage collected yet.
    Is this correct?

  2. If we don't unsubscribe in the servie, we will face the same problems as when we wouldn't unsubscribe in the component. Therefore B 1 - 3 are added (in fact the type how to unsubscribe is not important - only the fact we do - I choose the native RxJS approach)
    Is this correct?

(side note: I'm aware that it might be a better approach not to unsubscribe and handle the side effects manually in some cases, but lets ignore this so the question don't gets to broad)

  1. I don't know how .asObservable works internally, but there have to be some subscriptions as well.
    So is it necessary to complete the behaviour subjects (or normal subjects) like _someStuff (C.)?

Solution

  • If an observable(e.g: from HttpClient) completes, there is no need to unsubscribe.

    If the source of the observable completes or emits an error, the source will be unsubscribed automatically.

    For example, when you have something like this

    http.get(...)
     .pipe(
      a(),
      b(),
      c(),
     ).subscribe(observer)
    

    A few interesting things happen behind the scenes. First of all the observer is converted into a subscriber. The Subscriber class extends Subscription, which is holds the unsubscription logic.

    Each operator(a, b and c) in the chain there will have its own Subscriber instance, because after the first subscribe, each observable returned by their operator will be subscribed as well(an operator is a function which receives an observable and returns an observable). What you end up with is a chain of Subscribers.

    // S{n} -> Subscriber {n} (in the order in which they are created)
    http.get(...)
     .pipe(
      a(), // S4
      b(), // S3
      c(), // S2
     ).subscribe(observer) // S1
    

    S1 is the parent(destination) of S2, S2 is the parent of S3 and so forth.

    HttpClient.get is essentially the same as

    new Observable(subscriber => {
     // Make request
    
     // On request ready
     subscriber.next(resultOfTheRequest)
     subscriber.complete(); // So here completes!
    })
    

    Source Code

    What's important to mention is that, in this case, the subscriber parameter will be S4.

    subscriber.complete() is the same as S4.complete(). When this happens, S4 will pass along the complete notification to S3, which will in turn pass along the notification to S2 and so on until S1 receives the notification. At this point, it will unsubscribe.

    When a subscriber unsubscribes, all of its descendants will be unsubscribed as well.

    So, unsubscribing from a closed subscriber(it becomes closed after unsubscribing) is innocuous, but redundant.

    1.

    If a service is provided at root level(e.g: providedIn: 'root'), its ngOnDestroy will not be called. As far as I've noticed, it will be called only if the service is destroyed, which is the case when you provided it at component level.

    Here's a demo which illustrates this.

    IMO, if the service is provided at root level, you should not worry about unsubscribing from observables. If, however, your service is provided at component level, you should unsubscribe.

    2.

    As described in the previous sections, if your observables complete, no matter where your service is provided, you should not worry about unsubscribing.

    If your observables do not complete by themselves(e.g fromEvent) and the service is not provided at root level, you should manually unsubscribe in ngOnDestroy.

    3.

    A Subject will maintain a list of active subscribers. When an active subscriber becomes, well, inactive, it will be removed from the subscribers list.

    For instance, if you have a BehaviorSubject in a root-provided service, you should not worry about throwing away its references.

    Actually, a safer way to do this would be to call Subject.unsubscribe(). Subject.complete() will send the complete notification to its subscribers and then will empty out the list.