Why rxjs contains no onComplete operator, i.e., one that allows to do something when the source observable completes? The finalize operator works for both completion and error, while I need to only react to completion.
Yes, I know that I can use the onComplete callback of the subscribe() function, but this is something completely different. Sometimes you only want to do certain stuff on completion inside the rxjs pipeline, and not inside the subscribe call.
Am I missing something?
The tap operator accepts three arguments (value, error, completion)
ons$.pipe(
tap(null, null, () => console.log("Done")),
).subscribe()
or alternatively an observer, so you can do this:
obs$.pipe(
tap({ complete: () => console.log("Done") })
).subscribe()
See the operator docs for more: https://rxjs-dev.firebaseapp.com/api/operators/tap
I would recommend the observer syntax as this is the syntax that has been recommended by Ben Lesh in the past since it's more expressive.
Related
From the RxJS documentation I see the following example:
const source = interval(1000);
const clicks = fromEvent(document, 'click');
const result = source.pipe(takeUntil(clicks));
result.subscribe(x => console.log(x));
This is close to a code pattern needed for my app but I see a problem. The takeUntil operator subscribes, but as I understand it an Observer has no way to unsubscribe from the source Observable. It has no access to a Subscription object on which it can call unsubscribe().
So if I understand this correctly then once the user clicks the source observable will continue to emit ticks forever to the takeUntil which will consume them and do nothing with them.
Am I reading this correctly? If so is there a generally accepted way to kill the source observable from within the Observer pipe?
What happens with takeUntil is the following.
When the Observable passed to takeUntil as parameter notifies a value, the subscriber of the Observable returned by takeUntil completes and, as a consequence, all the subscriptions created in the pipe chain are unsubscribed one after the other in reverse order.
In simpler words, the unsubscription is performed behind the scene by the RxJs internal mechanisms.
To prove this behavior you can try this code
const source = interval(1000).pipe(
tap({ next: (val) => console.log('source value', val) })
);
const clicks = fromEvent(document, 'click');
const result = source.pipe(takeUntil(clicks));
result.subscribe((x) => console.log(x));
If you run it, you see that the message 'source value', val is printed until the click occurs. After this, no more message is printed on the console, which means that the Observable upstream, i.e. the Observable created by the interval function does not notify any more.
You can try the above code in this stackblitz.
SOME DETAILS ON THE INTERNALS
We can take a look at the internals of the RxJs implementation to see how this unsubscribe behind the scenes works.
Let's start from takeUntil. In its implementation we see a line like this
innerFrom(notifier).subscribe(new OperatorSubscriber(subscriber, () => subscriber.complete(), noop));
which, in essence, says that as soon as the notifier (i.e. the Observable passed to takeUntil as parameter) notifies, the complete method is called on the subscriber.
The invocation of the complete method triggers many things, but eventually it ends up calling the method execTeardown of Subscription which ends up invoking unsubscribe of OperatorSubscriber which itself calls unsubscribe of Subscription.
As we see, the chain is pretty long and complex to follow, but the core message is that the tearDown logic (i.e. the logic which is invoked when an Observable completes, errors or is unsubscribed) calls the unsubscription logic.
Maybe it is useful to look at one more thing, an implementation of a custom operator directly from the RxJs documentation.
In this case, at the end of the definition of the operator, we find this piece of code
// Return the teardown logic. This will be invoked when
// the result errors, completes, or is unsubscribed.
return () => {
subscription.unsubscribe();
// Clean up our timers.
for (const timerID of allTimerIDs) {
clearTimeout(timerID);
}
};
This is the teardown logic for this custom operator and such logic invokes the unsubscribe as well as any other cleanup activity.
I have an rxjs function using several rxjs operators from which I need to ultimately return an observable. My challenge is detailed below in the sample code with details of what I am trying to do. How can I accomplish this? Do I need a rewrite? Without the change I'm trying to make to access the values from Observable3 everything works as needed. I've tried many things including using withLatestFrom to bring in observable three and also combineLatest but to no luck.
Observable3 = of({obs3Prop1: value1, obs3Prop2: value2})
this.Observable1
.pipe(
switchMap(param1) => {
return this.getCount(param1);
}),
mergeMap((param2: number) =>
this.Observable2
.pipe(
//inside this pipe, return another observable from here using some rxjs operators
//where I need access to param2
//but I also need access to the value from Observable3
)
),
)
It seems like you could use just forkJoin() assuming the all source Observables complete:
mergeMap((param2: number) =>
forkJoin([
Observable3,
this.Observable2,
]).pipe(
mergeMap(([o3params, o2params]) => this.makeCall(o3params.param1, o2params)),
)
),
I'm rewriting some of my code to use Pipeable Operators in place of "patch" operators and I'm a little confused about when to use pipe() and how do I subscribe to a piped Observable to "activate" it if I only want side-effects?
For example, I have this code:
this.messages$ = this.messageService.getMessages(messageType)
.do((messages: Message[]) => {
console.log('The first message is ' + deviceMessages[0].text);
});
I get confused here because I figure I want to do this.messageService.getMessages(messageType).pipe(...) so I get an Observable returned and assigned to this.messages$ but then that won't execute because I haven't subscribe()'d. But if I do .pipe(...).subscribe() then it'll return a Subscription object, which isn't what I want.
What should I be doing here?
Well, do it simply like this:
this.messages$ = this.messageService.getMessages(messageType).pipe(
tap((messages: Message[]) => {
console.log('The first message is ' + deviceMessages[0].text);
})
);
this.messages$.subscribe();
the tap operator is the equivalent of .do() in rxjs 5 and above.
I have the following epic I use in my application to handle api requests:
action$ => {
return action$.ofType(actions.requestType)
.do(() => console.log('handled epic ' + actions.requestType))
.switchMap((action) => (
Observable.create((obs) => {
obs.next({ type: type, value: action.value, form: action.form });
})
.debounceTime(250)
.switchMap((iea) => (
Observable.ajax(ajaxPost(url(iea.value), body ? body(iea.value) : action.form))
.mergeMap(payload => {
return Observable.merge(
Observable.of(actions.success(payload)),
/* some other stuff */
);
})
.catch(payload => {
return [actions.failure(payload)];
})
))
))
.takeUntil(action$.filter((a) => (a.type === masterCancelAction))
.repeat();
};
Basically, any time I perform an api request, I dispatch a request action. If I dispatch another request quickly, the previous one is ignored using debounceTime. Additionally, the request can be cancelled using the masterCancelAction and when cancelled repeat() restarts the epic. This epic works as intended in all cases expect one.
The failure case occurs when a user uses the browser back during a request. In this case I fire the masterCancelAction to the request. However, on the same execution context as a result from the masterCancelAction, another request action dispatches to perform a new request on the same epic, but the api request does not occur (the console.log does occur though) as if there was no repeat(). In other cases where cancels occur, the next request is not invoked from the same execution context and it works fine, so it seems in this case my code does not give repeat a chance to restart the epic?
A dirty workaround I found was to use setTimeout(dispatch(action), 0) on the request that dispatches after the cancellation. This seems to allow repeat() to execute. I tried passing different schedulers into repeat, but that didn't seem to help. Also, attaching takeUntil and repeat into my inner switchMap solves the problem, but then other cases where my next request does not execute in the same call stack fail.
Is there a way I can solve this problem without using setTimeout? Maybe it is not a repeat related problem, but it seems to be the case.
Using rxjs 5.0.3 and redux-observable 0.14.1.
The issue is not 100% clear without something like a jsbin to see what you mean, but I do see some general issues that might help:
Anonymous Observable never completes
When creating a custom anonymous Observable it's important to call observer.complete() if you do indeed want it to complete. In most cases, not doing so will cause the subscription to be a memory leak and might also other strange behaviors
Observable.create((observer) => {
observer.next({ type: type, value: action.value, form: action.form });
observer.complete();
})
Observable.of would have been equivalent:
Observable.of({ type: type, value: action.value, form: action.form })
However, it's not clear why this was done as the values it emits are in captured in scope.
debounceTime in this case does not debounce, it delays
Since the anonymous observable it's applied to only ever emits a single item, debounceTime will act just as a regular .delay(250). I'm betting you intended instead to debounce actions.requestType actions, in which case you'd need to apply your debouncing outside the switchMap, after the action$.ofType(actions.requestType).
Observable.of accepts any number of arguments to emit
This is more of a "did you know?" rather than an issue, but I noticed you're merging your of and /* some other actions */ I assume would be other of observables merged in. Instead, you can just return a single of and pass the actions as arguments.
Observable.of(
actions.success(payload),
/* some other actions */
actions.someOtherOne(),
actions.etc()
);
Also, when you find yourself emitting multiple actions synchronously like this, consider whether your reducers should be listening for the same, single action instead of having two or more. Sometimes this wouldn't make sense as you want them to have completely unrelated actions, just something to keep in mind that people often forget--that all reducers receive all actions and so multiple reducers can change their state from the same action.
.takeUntil will stop the epic from listening for future actions
Placing the takeUntil on the top-level observable chain causes the epic to stop listening for action$.ofType(actions.requestType), which is why you added the .repeat() after. This might work in some cases, but it's inefficient and can cause other hard to realize bugs. Epics should be thought of instead as sort of like sidecar processes that usually "start up" with the app and then continue listening for a particular action until the app "shuts down" aka the user leaves the app. They aren't actually processes, it's just helpful to conceptually think of them this way as an abstraction.
So each time it matches its particular action it then most often will switchMap, mergeMap, concatMap, or exhaustMap into some side effect, like an ajax call. That inner observable chain is what you want to make cancellable. So you'd place your .takeUntil on it, at the appropriate place in the chain.
Summary
As mentioned, it's not clear what you intended to do and what the issue is, without a more complete example like a jsbin. But strictly based on the code provided, this is my guesstimate:
const someRequestEpic = action$ => {
return action$.ofType(actions.requestType)
.debounceTime(250)
.do(() => console.log('handled epic ' + actions.requestType))
.switchMap((action) =>
Observable.ajax(ajaxPost(url(action.value), body ? body(action.value) : action.form))
.takeUntil(action$.ofType(masterCancelAction))
.mergeMap(payload => {
return Observable.of(
actions.success(payload),
/* some other actions */
...etc
);
})
.catch(payload => Observable.of(
actions.failure(payload)
))
);
};
Check out the Cancellation page in the redux-observable docs.
If this is a bit confusing, I'd recommend digging a bit deeper into what Observables are and what an "operator" is and does so that it doesn't feel magical and where you should place an operator makes more sense.
Ben's post on Learning Observable by Building Observable is a good start.
Nothing too much serious here, just curious.
I wanted to make an example for something and came up with this code :
const { Observable, Subject } = Rx
const timeout$ = new Subject()
const obs$ = Observable
.of(1)
.takeUntil(timeout$)
.delay(2000)
.subscribe(x => console.log(x))
timeout$.next()
timeout$.complete()
I thought this code would not display the console.log but it does.
Can someone explain me why ?
Is this a bug or just a bad understanding from me about takeUntil ?
Here's a Plunkr to demo https://plnkr.co/edit/wpKztBabnBeIuNZS28wu?p=info
Notice that if you switch the order of the takeUntil() and delay() it won't be emitted as expected:
Observable
.of(1)
.delay(2000)
.takeUntil(timeout$)
.subscribe(x => console.log(x));
What you see happens because the delay() schedules the emission before that takeUntil() sends complete notification.
Right now in RxJS 5.2.0 the takeUntil() operator doesn't force unsubscribe from its source which is the problem here. So when the takeUntil() is notified by timeout$ it remains subscribed to its source Observable and therefore the scheduled emission from delay() isn't disposed.
Notice there's no this.unsubscribe() call when handling emission from the notification Observable: https://github.com/ReactiveX/rxjs/blob/master/src/operator/takeUntil.ts#L61
There're other operators with the same behavior. For example first() and takeWhile().
On the other hand for example the take() operator does unsubscribe:
https://github.com/ReactiveX/rxjs/blob/master/src/operator/take.ts#L80
This behavior is in fact reported and whether this is a bug or not is being discussed right now:
https://github.com/ReactiveX/rxjs/issues/2455
https://github.com/ReactiveX/rxjs/pull/2470
https://github.com/ReactiveX/rxjs/pull/2463
https://github.com/ReactiveX/rxjs/pull/2457