RxJS takeUntil doesn't unsubscribe - javascript

I want to unsubscribe in declarative style with "takeUntil" operator. But that's basically does't work. I can see console output anyway.
const unsubscribe = new Subject();
function printFoo() {
of('foo')
.pipe(takeUntil(unsubscribe))
.subscribe(console.log) // Why I can see 'foo' in the console?
}
function onDestroy() {
unsubscribe.next();
unsubscribe.complete();
}
onDestroy()
setTimeout(() => printFoo(), 200)
StaackBlitz:
https://stackblitz.com/edit/rxjs-svfkxg?file=index.ts
P.S. I expected that even unsubscribe.next() would be enough to unsubscribe, but even with unsubscribe.complete() it doesn't work.

You're calling onDestroy() before the chain with takeUntil is even created.
When you eventually call printFoo() the previous emissions to unsubscribe won't be re-emited and the Subject unsubscribe is already completed anyway so takeUntil in this case will never complete the chain.

Because the Subject emits before printFoo subscription.
After you subscribe there are no more Subject emittions.
You could use BehaviorSubject instead, since it holds the emitted values (the last
emitted value):
const unsubscribe = new BehaviorSubject(false);
function printFoo() {
of('foo')
.pipe(takeUntil(unsubscribe.pipe(filter(value => !!value)))) // Don't unsub if it's false emitted
.subscribe(console.log)
}
function onDestroy() {
unsubscribe2.next(true); // Emit true to cancel subscription
}

Related

How to execute same function on subsciption and error callback in rxjs to wrap loading state, i.e. show loading before Observable and hide afterwards?

Use case
I have some loading that starts in an RxJS Observable and I want to end/wrap that loading when finished.
Th observable either returns an error or it returns new data to load.
Finishing can mean both the Observable returns an error or it triggers a new value.
Edit: In the variant below, I additionally have another Observable that triggers the whole loading process.
MVE
You can trivially implement it by duplicating the code, but that is
const {
from,
concat,
throwError
} = rxjs;
concat(
from([1, 2, 3]),
throwError("fail")
).subscribe((x) => {
console.log("loading end", x);
},
(x) => {
console.log("loading end", x);
}
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.7/rxjs.umd.min.js"></script>
Basically I search for an operator or so that would remove the code duplication in the above example.
With loading showing
Showing loading of course happens before, e.g. in a tap:
const {
from,
concat,
throwError,
pipe,
tap
} = rxjs;
concat(
from([1, 2, 3]),
throwError("fail")
).pipe(
tap(() => console.log("show loading"))
).subscribe((x) => {
console.log("loading end", x);
},
(x) => {
console.log("loading end", x);
}
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.7/rxjs.umd.min.js"></script>
Uff tap i.e. showing the loading of course also not happens on error.
However, this is the real example
However, in my real implementation that won't happen, because I merge two Observables and have a trigger Observable that triggers the whole process:
const {
from,
concat,
throwError,
pipe,
tap,
switchMap,
Subject
} = rxjs;
// this is the trigger to load stuff (in Angular triggered from the outside component e.g.)
loadData$ = new Subject();
loadData$ //< <- this is irrelevant now and just the trigger
.pipe(
tap(() => console.log("show loading")), // <-- show loading when/before data loading starts
switchMap(() => {
return concat(
from([4, 5, 6]),
throwError("fail")
)
})
).subscribe((x) => { // <-- end loading when/after data loading ends or fails
console.log("loading end", x);
},
(x) => {
console.log("loading end", x);
}
)
loadData$.next(); // just trigger for testing here
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.7/rxjs.umd.min.js"></script>
Tries
I tried the finalize operator, but that only triggers on completion (whether error or not), so that is not an option here.
I guess the complete callback would thus have the same effect.
Okay, I see problems...
While writing this, admittedly, I see that if the Observable fails, no new values may be emitted. (Or may that be the case, actually?)
const {
from,
concat,
throwError,
of
} = rxjs;
concat(
from([1, 2, 3]),
throwError("fail"),
of ("another value") // <-- is not triggered
).subscribe((x) => {
console.log("loading end", x);
},
(x) => {
console.log("loading end", x);
}
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.7/rxjs.umd.min.js"></script>
Interne Referenz: !10155
Your update is correct. finalize is the way to go here.
From rxjs docs (https://rxjs.dev/guide/observable) you can see the following quote:
In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards.
This means if an error is thrown, finalize is called. This observable will not emit anything else afterwards, so the state is the same as completed.

How to subscribe to an observable once?

My function access() needs to subscribe once, each and every call.
In the snippet below, $valueChanges emits data to each change made. Calling access() without const $ = ... and $.unsubscribe(), $valueChanges observable emits unnecessary streams of values.
Is there an rxjs operator/function that emits once in subscription inside a function? Even if the function is called multiple times the subscription emits once?
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
).subscribe((res) => {
...
$.unsubscribe();
});
}
You can consider using the take() operator, and emit only the first value before completing.
According to the documentation, the take operator
Emit provided number of values before completing.
This is how you can use it:
access() {
valueChanges
.pipe(
map((res) =>
...
),
take(1),
).subscribe((res) => {
...
});
}
Try shareReply(1). Then the original stream will be called only once and its emit will be shared with all subscribers. If the stream emits 2nd time - the update will go to all subscribers too.
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
// take(1), // in case if you need just 1 emit from it.
shareReply(1), // in case if you don't want to trigger `$valueChanges` on every subscription.
).subscribe((res) => {
...
// $.unsubscribe(); // it's useless in case of `take(1)`.
});
}

switchMap distincted by property in RxJs

Let's say, I have a stream of actions. Each action is assigned some id. Like this:
const actions$ = of({ id: 1 }, { id: 2 }, { id: 1 });
Now, for each action, I want to perform some logic in switchMap:
actions$.pipe(switchMap(a => /* some cancellable logic */)).subscribe(...);
The problem is that each emitted action cancels previous 'some cancellable logic'.
Is it possible to cancel 'some cancellable logic' based on action id, preferably an operator? Something like:
actions$.pipe(switchMapBy('id', a => /*some cancellable logic */)).subscribe(...)
Essentially, current behaviour with switchMap:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap unsubscribes from previous nested observable. Subscribes to new one.
3. actions$ emits id #1. switchMap again unsubscribes from previous nested observable. Subscribes to new one.
Expected behaviour:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap again subscribes to nested observable (this time with #2). And here's the difference, it doesn't cancel the one from #1.
3. actions$ emits id #1. switchMap unsubscribes from nested observable for #1. Subscribes again, for #1.
this seems to be a use case for the mergeMap operator. The use case of switchMap is to only maintain one inner subscription and cancel previous ones, which is not what you're after here. You want multiple inner subscriptions and you want them to cancel when a new value of the same id comes through, so implement some custom logic to do that.
something along the lines of:
action$.pipe(
mergeMap(val => {
return (/* your transform logic here */)
.pipe(takeUntil(action$.pipe(filter(a => a.id === val.id)))); // cancel it when the same id comes back through, put this operator at the correct point in the chain
})
)
you can turn this into something resuable by writing a custom operator:
import { OperatorFunction, Observable, from } from 'rxjs';
import { takeUntil, filter, mergeMap } from 'rxjs/operators';
export function switchMapBy<T, R>(
key: keyof T,
mapFn: (val: T) => Observable<R> | Promise<R>
): OperatorFunction<T, R> {
return input$ => input$.pipe(
mergeMap(val =>
from(mapFn(val)).pipe(
takeUntil(input$.pipe(filter(i => i[key] === val[key])))
)
)
);
}
and use it like:
action$.pipe(
switchMapBy('id', (val) => /* your transform logic here */)
);
here's a blitz of it in action: https://stackblitz.com/edit/rxjs-x1g4vc?file=index.ts
use filter operation before switchMap to exclude canceled ids, like this
of({ id: 1 }, { id: 2 }, { id: 1 }).pipe(
filter(id => ![1,2].includes(id)), // to exclude ids (1,2)
switchMap(id => /*some cancellable logic */ )
).subscribe(...)

(Angular) How to return different Observable type request in my case?

I am having trouble to return an observable. It seems like the codes inside the mergeMap is not running at all.
Codes:
book.service.ts
import {HttpClient, HttpHeaders} from '#angular/common/http';
export class bookService {
constructor(
private http: HttpClient,
...others
) {}
addNewBook(book): Observable<Book>{
##### Tried to use mergeMap otherwise the return type won't match
return this.tokenService.getToken().mergeMap((token: string) => {
console.log("Fire this...") <===== This is never output.
const myUrl = "www.testurl.com";
const parameters = {
bookTitle: book.name,
};
return this.http.post<Book>(myUrl, book);
})
}
token.service.ts
public token$: Subject<string>;
..others
public getToken(): Observable<string> {
return this.token$; <= return Observable<string> not Observable<Book>
}
book.component.ts that calls the addNewBook method.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book);
}).then((result) => {
console.log(result);
})
I can't really change the token service because it's used on other place, I am not sure why the codes inside the mergeMap is not running. Can someone help me about it? Thanks a lot!
It won't work unless you subscribe to the results of bookService.addNewBook(book). Just returning it from the then callback won't subscribe. You need to at least add toPromise.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book).toPromise();
}).then((result) => {
console.log(result);
})
In order for the mergeMap() to be be triggered, the token$ subject inside token.service.ts needs to emit a value (via .next()) after addNewBook() is subscribed to by a consumer.
One of the things to keep in mind with Subjects is that 'late subscribers' won't receive a value off of them until the next time .next([value]) is called on that Subject. If each subscriber, no matter how late, needs to immediately receive the last value generated by that source (Subject) then you can use BehaviorSubject instead.
From your short code example it is hard to see where the Observable generated by addNewBook() is being subscribed to though. Remember, a Observable won't execute until it has a subscriber.

Rx BehaviorSubject + scan pushing prior event to new subscriber?

I want to have an stream to which I can push reducer functions. Each time a reducer function is pushed, a state object should be passed to the reducer, the reducer should return a modified state value, and the updated state should be pushed to subscribers. I'm hoping my code can explain:
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.BehaviorSubject(Rx.helpers.identity);
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);
Here's the output I see:
subscriptionA 1
subscriptionA 2
subscriptionB 1
while I was hoping to see:
subscriptionA 1
subscriptionA 2
subscriptionB 2
Obviously I'm missing something fundamental here. It seems the BehaviorSubject is supposed to retain the latest value for new subscribers which would make me think that when subscriptionB subscribes to downstream that it would get the latest reduced value, but it looks like having the .scan in the middle fouls things up...or something.
What's going on here and how to I accomplish what I'm trying to accomplish? Thanks!
Can you try to see if everything is conform to your expectations if you replace
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
by
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState).shareReplay(1);
jsfiddle here : http://jsfiddle.net/cqaumutp/
If so, you are another victim of the hot vs. cold nature of Rx.Observable, or maybe more accurately the lazy instantiation of observables.
In short (not so short), what happens everytime you do a subscribe, is that a chain of observables is created by going upstream the chain of operators. Each operator subscribes to its source, and returns another observable up to the starting source. In your case, when you subscribe to scan, scan subscribes to upstream which is the last one. upstream being a subject, on subscription it just registers the subscriber. Other sources would do other things (like register a listener on a DOM node, or a socket, or whatever).
The point here is that every time you subscribe to the scan, you start anew, i.e. with the initialState. If you want to use the values from the first subscription to the scan, you have to use the share operator. On the first subscription to the share, it will pass your subscription request on to the scan. On the second and subsequent ones, it will not, it will register it, and forward to the associated observer all the values coming from the scan firstly subscribed to.
I have a solution that seems to be giving me the results I'm looking for. I would appreciate it if others could verify that it's an appropriate solution.
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.Subject();
let downstreamSource = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
export let downstream = new Rx.BehaviorSubject(initialState);
downstreamSource.subscribe(downstream);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);

Categories

Resources