I'm relatively new to RXJS so I apologize if this is a repeat. I tried searching for answers but wasn't able to find any, potentially because I don't know which search terms to use.
I'm trying to understand how I can know when the finalize block of a service call completes, because it updates a shared state variable.
Here is the stackblitz of it, though I'll also post snippets below: https://stackblitz.com/edit/angular-ivy-xzvkjl
I have an Angular application with a service that sets a shared isLoading
flag to true, kicks off an HTTP request, then uses finalize
to set the isLoading
flag back to false, so that regardless of success or error, the items that check the isLoading
flag know that the HTTP request is no longer processing.
I've simplified that scenario into separate methods instead of separate classes:
isLoading = false;
public ngOnInit() {
this.serviceCall().subscribe(
next => {
console.log("value of isLoading in next handler: " + this.isLoading);
},
err => {
console.log("value of isLoading in error handler: " + this.isLoading);
},
() => {
console.log("value of isLoading in complete handler: " + this.isLoading);
}
);
}
private serviceCall() {
this.isLoading = true;
return this.httpCall().pipe(
tap(value => console.log(value)),
finalize(() => {
this.isLoading = false;
console.log("Value of isLoading in serviceCall finalize: " + this.isLoading);
})
);
}
private httpCall() {
return new Observable(subscriber => {
console.log("Starting emissions");
subscriber.next(42);
subscriber.next(100);
subscriber.next(200);
console.log("Completing emissions");
subscriber.complete();
});
}
I was surprised to find that the output of this example is
Starting emissions
42
value of isLoading in next handler: true
100
value of isLoading in next handler: true
200
value of isLoading in next handler: true
Completing emissions
value of isLoading in complete handler: true
Value of isLoading in serviceCall finalize: false
Why is the finalize
of the serviceCall
invoked AFTER the complete handler of the ngOnInit
's subscribe block? And how am I supposed to know when the serviceCall
has completed its manipulation of the shared variable if not through a completed handler?
finalize
This comes down to how finalize
is implemented. I agree that maybe this isn't really intuitive. I'm part of a split faction that the believes the way it's implemented now is the intuitive way.
Consider an observable that is unsubscribed before it emits anything. I would expect finalize to still trigger, but I wouldn't expect a complete
notification to be sent to my observer.
Six of one, half a dozen of the other
Generally, the final thing that happens to a stream is that it is unsubscribed from. Finalize gets invoked when a stream is unsubscribed. This happens after a complete or error emission.
You can think of finalize as what happens during tear-down of an observable. Whereas an observer is observing the emissions of an observable that still exists.
In general, side effects like setting global variables and checking them later in the same pipeline
are considered code smells. If, instead, you lean a bit harder into the functional approach that RxJS streams advocate, issues like this should disappear.
Timing asynchronous events can often lead to strange or unexpected results (part of the reason you really shouldn't implement that kind of thing manually if you can help it).
Consider what happens when I add a delay into your stream:
private serviceCall() {
this.isLoading = true;
return this.httpCall().pipe(
tap(value => console.log(value)),
finalize(() => {
this.isLoading = false;
console.log("Value of isLoading in serviceCall finalize: " + this.isLoading);
}),
delay(0)
);
}
You would think a delay of 0 milliseconds should make no difference, but because every delay gets put on JS's microtask queue, you'll notice a marked difference in how your code runs. Before your subscribe gets called with the first value, isLoading
will already be false.
That's because everything before the delay is run synchronously and will complete before the microtask queue is run. Everything after delay(0)
is run asynchronously and will complete the next time JS is ready to run the microtask queue.
This isn't idiomatic RxJS, but it'll work the way you expect finalize to work in this case.
You can use the tap operator to catch a complete
emission. Since tap will trigger on complete
, this should fire before subscribe
and therefore work for your use-case.
function serviceCall() {
const setIsLoading = bool => (_ = null) => this.isLoading = bool;
return defer(() => {
setIsLoading(true)();
return this.httpCall().pipe(
tap({
next: console.log,
error: setIsLoading(false),
complete: setIsLoading(false)
})
);
});
}