Yusuf Azal
Yusuf Azal

Reputation: 228

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();
  }
});

Upvotes: 4

Views: 1825

Answers (3)

Mrk Sef
Mrk Sef

Reputation: 8062

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();

Upvotes: 6

Srikar Phani Kumar M
Srikar Phani Kumar M

Reputation: 1384

There are various ways of unsubscribing data:

Method 1: Unsubscribe after subscription; (Not preferred)

let localSubscription = someObservable.subscribe(result => {
  this.result = result;
}).unsubscribe();
---------------------
Method 2: If you want only first one or 2 values, use take operator or first operator

a) let localSubscription = 
  someObservable.pipe(take(1)).subscribe(result => {
     this.result = result;
  });

b) let localSubscription = 
  someObservable.pipe(first()).subscribe(result => {
     this.result = result;
  });
---------------------
Method 3: Use Subscription and unsubscribe in your ngOnDestroy();

let localSubscription = 
  someObservable.subscribe(result => {
     this.result = result;
  });

ngOnDestroy() { this.localSubscription.unsubscribe() }
----------------------

Method 4: Use Subject and takeUntil Operator and destroy in ngOnDestroy

let destroySubject: Subject<any> = new Subject();

let localSubscription = 
  someObservable.pipe(takeUntil(this.destroySubject)).subscribe(result => {
     this.result = result;
  });

ngOnDestroy() { 
  this.destroySubject.next();
  this.destroySubject.complete();
}

I would personally prefer method 4, because you can use the same destroy subject for multiple subscriptions if you have in a single page.

Upvotes: -1

Anton Marinenko
Anton Marinenko

Reputation: 2992

It's working method, but RxJS mainly recommend use async pipe in Angular. That's the perfect solution. In your example you assign result to the object property and that's not a good practice.

If you use your variable in the template, then just use async pipe. If you don't, just make it observable in that way:

private readonly result$ = someObservable.pipe(/...get exactly what you need here.../)

And then you can use your result$ in cases when you need it: in other observable or template.

Also you can use pipe(take(1)) or pipe(first()) for unsubscribing. There are also some other pipe methods allowing you unsubscribe without additional code.

Upvotes: -1

Related Questions