rxjsobservableunsubscribe

Observable unsubscribe inside subscribe method


I have tried to unsubscribe within the subscribe method. It seems like it works, I haven't found an example on the internet that you can do it this way.

I know that there are many other possibilities to unsubscribe the method or to limit it with pipes. Please do not suggest any other solution, but answer why you shouldn't do that or is it a possible way ?

example:

let localSubscription = someObservable.subscribe(result => {
  this.result = result;
  if (localSubscription && someStatement) {
    localSubscription.unsubscribe();
  }
});

Solution

  • The problem

    Sometimes the pattern you used above will work and sometimes it won't. Here are two examples, you can try to run them yourself. One will throw an error and the other will not.

    const subscription = of(1,2,3,4,5).pipe(
      tap(console.log)
    ).subscribe(v => {
      if(v === 4) subscription.unsubscribe();
    });
    

    The output:

    1
    2
    3
    4
    Error: Cannot access 'subscription' before initialization
    

    Something similar:

    const subscription = of(1,2,3,4,5).pipe(
      tap(console.log),
      delay(0)
    ).subscribe(v => {
      if (v === 4) subscription.unsubscribe();
    });
    

    The output:

    1
    2
    3
    4
    

    This time you don't get an error, but you also unsubscribed before the 5 was emitted from the source observable of(1,2,3,4,5)

    Hidden Constraints

    If you're familiar with Schedulers in RxJS, you might immediately be able to spot the extra hidden information that allows one example to work while the other doesn't.

    delay (Even a delay of 0 milliseconds) returns an Observable that uses an asynchronous scheduler. This means, in effect, that the current block of code will finish execution before the delayed observable has a chance to emit.

    This guarantees that in a single-threaded environment (like the Javascript runtime found in browsers currently) your subscription has been initialized.

    The Solutions

    1. Keep a fragile codebase

    One possible solution is to just ignore common wisdom and continue to use this pattern for unsubscribing. To do so, you and anyone on your team that might use your code for reference or might someday need to maintain your code must take on the extra cognitive load of remembering which observable use the correct scheduler.

    Changing how an observable transforms data in one part of your application may cause unexpected errors in every part of the application that relies on this data being supplied by an asynchronous scheduler.

    For example: code that runs fine when querying a server may break when synchronously returned a cashed result. What seems like an optimization, now wreaks havoc in your codebase. When this sort of error appears, the source can be rather difficult to track down.

    Finally, if ever browsers (or you're running code in Node.js) start to support multi-threaded environments, your code will either have to make do without that enhancement or be re-written.

    2. Making "unsubscribe inside subscription callback" a safe pattern

    Idiomatic RxJS code tries to be schedular agnostic wherever possible.

    Here is how you might use the pattern above without worrying about which scheduler an observable is using. This is effectively scheduler agnostic, though it likely complicates a rather simple task much more than it needs to.

    const stream = publish()(of(1,2,3,4,5));
    
    const subscription = stream.pipe(
      tap(console.log)
    ).subscribe(x => {
      if(x === 4) subscription.unsubscribe();
    });
    
    stream.connect();
    

    This lets you use a "unsubscribe inside a subscription" pattern safely. This will always work regardless of the scheduler and would continue to work if (for example) you put your code in a multi-threaded environment (The delay example above may break, but this will not).

    3. RxJS Operators

    The best solutions will be those that use operators that handle subscription/unsubscription on your behalf. They require no extra cognitive load in the best circumstances and manage to contain/manage errors relatively well (less spooky action at a distance) in the more exotic circumstances.

    Most higher-order operators do this (concat, merge, concatMap, switchMap, mergeMap, ect). Other operators like take, takeUntil, takeWhile, ect let you use a more declarative style to manage subscriptions.

    Where possible, these are preferable as they're all less likely to cause strange errors or confusion within a team that is using them.

    The examples above re-written:

    of(1,2,3,4,5).pipe(
      tap(console.log)
      first(v => v === 4)
    ).subscribe();