17

In Angular 2 there are some observables that you do not need to unsubscribe from. For example http requests and activatedRoute.params.

Angular/RxJs When should I unsubscribe from `Subscription`

But what happens when I use switchMap on for example activatedRoute.params and inside that switchMap I access a service that returns an observable that would need to be unsubscribed if subscribed to in the usual way.

Something like this:

this.activatedRoute.params
    .switchMap((params: Params) => this.userService.getUser(+params['id']))
    .subscribe((user: User) => this.user = user);

If I called this.userService without the switchMap and without activatedRoute.params I would have to unsubscribe from it.

// userService.getUser() takes in optional id?: number.
this.subscription = this.userService.getUser().subscribe(
    (user: User) => {
        this.user = user;
    }
 );

And then later..

this.subscription.unsubscribe();

My question is, do I need to unsubscribe from activatedRoute.params if I use switchMap on it and call a service that would need to be unsubscribed otherwise?

Alex
  • 5,671
  • 9
  • 41
  • 81

5 Answers5

16

If the source observable to which you are subscribing always completes or errors, you don't need to unsubscribe.

However, if you compose another observable from the source using switchMap, whether or not you need to unsubscribe depends upon the observable returned within switchMap. If the returned observable does not always complete or error, then, yes, you will need to unsubscribe.

If the source errors, an automatic unsubscription will occur:

const source = new Rx.Subject();
const composed = source.switchMap(() => Rx.Observable.interval(200));

composed.subscribe(value => console.log(value));
source.next(1);

setTimeout(() => {
  console.log("Erroring...");
  source.error(new Error("Boom!"));
}, 1000);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@5.4.1/bundles/Rx.min.js"></script>

However, if the source completes, an automatic unsubscription will not occur:

const source = new Rx.Subject();
const composed = source.switchMap(() => Rx.Observable.interval(200));

composed.subscribe(value => console.log(value));
source.next(1);

setTimeout(() => {
  console.log("Completing...");
  source.complete();
}, 1000);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@5.4.1/bundles/Rx.min.js"></script>
cartant
  • 57,105
  • 17
  • 163
  • 197
1

switchMap creates a link between the previous and new observable. If you change the first observable, the second will be always be triggered.

Anything subscribed after the switchMap will be hearing changes on both, the initial observable and the returned observable.

To fully stop the first observable to update the second one or the rest is by using take, takeUntil, or takeWhile. Like:

const howTimerWorks = interval(5000).pipe(
  take(2), // only get 2 responses after 5 seconds each
  switchMap(initialNumber => interval(1000)));

// 0 after 5s, then 1, 2 , 3, (new Subs) 0, 1, ... every sec, forever now.
howTimerWorks.subscribe(console.log)

Ignacio Bustos
  • 1,415
  • 2
  • 17
  • 26
0

New RxJS documentation explains when switchMap will continue listening and when will stop during the exception propagation.

See iWillContinueListening and iWillStopListening demo:

https://www.learnrxjs.io/learn-rxjs/operators/error_handling/catch

Ievgen
  • 4,261
  • 7
  • 75
  • 124
0

If the returned observable does not complete then an unsubscribe may be necessary.

This answer suggests this method should be used https://stackoverflow.com/a/41177163/6088194

The solution we should all use going forward is to add a private ngUnsubscribe = new Subject(); field to all components that have .subscribe() calls to Observables within their class code.

We then call this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); in our ngOnDestroy() methods.

The secret sauce (as noted already by @metamaker) is to call takeUntil(this.ngUnsubscribe) before each of our .subscribe() calls which will guarantee all subscriptions will be cleaned up when the component is destroyed.

However, according to this article it has potential for memory leaks when used with switchMap if the takeUntil operator is not called last in the sequence. So if you use this solution with switchMap then make sure takeUntil is called last in the sequence.

private ngUnsubscribe = new Subject();

ngOnDestroy() {
    // unsubscribe to any subscriptions that use takeUntil(ngUnsubscribe)
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

someFunction() {
    this.activatedRoute.params.pipe(
        switchMap((params: Params) => this.userService.getUser(+params['id'])),
        takeUntil(this.ngUnsubscribe)
    )
     .subscribe((user: User) => this.user = user);
}

I suggest reading both the linked answer and the linked article as it is very helpful information.

-2

Once you do a switchmap, the Subscription is attached to the last Observable. It doesn't matters if the first observable keeps triggering. The switchmap block is only executed once.

You have to unsusbscribe from the last if this never closes.

Check this code:

import { Component, OnInit } from '@angular/core';
import { Observable, BehaviorSubject, Subscription} from 'rxjs';
import * as Rx from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';
  private source: Rx.Subject<any>;
  private source2: Rx.Subject<any>;
  private composed: Rx.Observable<any>;
  private composedSub: Subscription;

  public ngOnInit(): void {
    this.source = new Rx.Subject();
    this.source2 = new Rx.Subject();

    this.composed = this.source.switchMap(value => this.source2);

    this.composedSub = this.composed.subscribe(value => console.log(value));
    console.log(this.composedSub);
  }

  private onClick() {
    // Triggers the first observable, the console.log is never executed.
    this.source.next(1);
  }

  private onClick2() {
    // Console.log is executed, prints "1"
    this.source2.next(1);
  }

  private onClick3() {
    // The console log is never called again after click on this button.
    this.composedSub.unsubscribe();
  }

  private onClick4() {
    // The first observable finish. the console log keeps printing unles onClick3 is executed
    this.source.complete();
  }

  private onClick5() {
    // console.log never executed egain.
    this.source2.complete();
  }
}
Werem
  • 534
  • 2
  • 5
  • 16
  • Hello, don't add a comment to another answer as your own answer. If you can answer the question, then you should focus on problem. If you have some remarks to another answer then just comment it. – franiis Aug 08 '18 at 09:59
  • This is not true, any change in 'this.source' will update 'this.source2', you are just ignoring the value on subject 1 and therefore the second is not getting updated, but any switchMap that modifies the status of the second will be triggered again when the first changes. – Ignacio Bustos Jun 10 '20 at 17:40