Search code examples
angularrxjsobservableorder-of-execution

RXJS How to Know When Finalize Completes?


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?


Solution

  • About 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.


    Avoid side effects where possible

    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.


    Quick Aside:

    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.


    Solution of Least Resistance

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