5
votes

I have a question about one of the common pattern for the unsubscribing with the takeUntil operator for Angular and RxJs. In this article, it 's under the third position. For example, we have such code in a component class:

  private destroy$: Subject<boolean> = new Subject();

  ngOnInit() {
     this.control.
     .pipe(takeUntil(this.destroy$)
     .subscribe(doSmthngFunc); 
  }

  ngOnDestroy() {
    this.destroy$.next(true);
    // Which next line of code is correct?
    // this.destroy$.complete()     // this one?
    // this.destroy$.unsubscribe()  // or this one?
  }

The first line this.destroy$.next(true) is totally clear. But the second is not. If we look into the realization of these methods, we find that they have somewhat similar behavior. complete(): unsubscribe():

As I understand semantically complete() is preferable, because we call next() for the first and the last time during the component life and then we finished with this Subject, treated as Observable and can invoke complete(). These methods belong to the observer and unsubscribe belong to the observable, and we have no subscriptions to unsubscribe from. But under the hood, these methods have a similar code:

    this.isStopped = true; // both

    this.observers.length = 0; // complete
    this.observers = null;     // unsubscribe

    this.closed = true;        // only unsubscribe

Theoretically complete() has delayed effect as it's may invoke complete() on every observer subscribed, but we have no observers on destroy$. So the question - which way is more preferable, less error-prone, and why?

3
Both of the lines beneath the Which next line of code is correct? comment are redundant. Only the this.destroy$.next(true); is necessary. The takeUntil operator is the only subscriber to the destroy$ subject and it will unsubscribe as soon as the destroy$ emits the next notification. Neither complete nor unsubscribe need to be called. And you almost never really want to call unsubscribe on a subject - see blog.angularindepth.com/rxjs-closed-subjects-1b6f76c1b63ccartant
but we have no observers on destroy$ every time you use takeUntil() an observer is added. Observers are the things listening. I think you misunderstood the term.Reactgular
The blog post link specified above is no longer valid. The canonical link to the post is: ncjamieson.com/closed-subjectscartant

3 Answers

1
votes

Destruction of a component is a singular event.

   this.destroy$.next();
   this.destroy$.complete();

Ensures that the subject emits only once and completes.

For example;

    const destroy$ = new Subject();

    destroy$.subscribe(v => console.log("destroyed"));

    destroy$.next();
    destroy$.complete();
    destroy$.next();

    // the above prints "destroyed" only once.

It's not a technical requirement to complete, but if you don't then business logic that depends upon completion instead of emission will not always work, and might leak memory.

For example, the following would be a memory leak in RxJs.

   destroyed$.subscribe(() => {
       console.log('This might leak memory');
   });

The above could leak memory because the subscription never ends and the observable never completes. You can fix the leak by adding a first() operator or making sure the subject is completed. RxJS does not know that the subject will only emit one value, and so you have to tell it. Otherwise subscribers remain bound to the stack frame and are not garbage collected. So while the garbage collector might collect the component after it is used if anything references the stack frame of the subscriber, then that subscription lives on.

So call complete on your destroy subjects so that other people don't make mistakes.

this.destroy$.unsubscribe()

Calling unsubscribe on a subject might not have an effect on downstream operators that create inner subscriptions. For example, switchMap() and mergeMap() create inner subscriptions.

So you can not manage subscriptions higher up effectively. It's better to unsubscribe from the subscription created when you call the subscribe() method, because this is last in the chain of operators.

0
votes

I do not think you need to do much more than destroy$.next();. destroy$ is a Subject you created in your class, and the only thing it is responsible for is aborting your subscription. No one, but your class is allowed to touch it (since it is private)

According to the article, it is better to do a destroy$.complete() to avoid memory leaks. I do not think using unsubscribe() on it makes any sense. If someone subscribed to destroy$, you should store that in a Subscription, and unsubscribe in the callers ngOnDestroy()-method. However, since you are using takeUntil, there is no need for an unsubscribe.

-1
votes

Both statements will have the desired effect. Afaik there's no technical reason not to use unsubscribe. Semantically - as you mentioned - it makes a lot more sense to use complete.

A Subject is both an observable (sending) and an observer (listening), and that duality is reflected here. next and complete both belong to the 'sending' side of things. complete signals 'this stream will not send any further values'. unsubscribe is part of the 'listening' interface and has a correspondingly different meaning: 'don't notify me of any further emissions'.

Edit: on re-reading I see you've already included this distinction in the question, so my answer probably doesn't add much for you :(. I do think the semantics is important enough in its own right for using complete over unsubscribe here, and can see no actual risk of using the one over the other in this pattern. Hope that's still somewhat helpful!