Using Observables, show loading indicator after delay but cancel if loading is completed in time?

You can write something along these lines:

function getCustomer(id) {
    return Observable.of({'name': 'John', id}).delay(500);
}

Observable.of({'id': 42})
    .distinctUntilChanged(params => params['id'])
    .do(() => {
        // this.errorMessage = '';
    })
    .switchMap((params) => {
        return Observable.combineLatest(
            Observable.of(true).delay(300).startWith(null), // delay Observable
            getCustomer(params['id']).startWith(null), // customer Observable
            function(delay, customer) { // selector function
                if (customer) {
                    return customer;
                }

                if (delay && !customer) {
                    console.log('this.isLoading = true;');
                }
                return null;
            })
            .filter(customer => customer)
            .distinctUntilChanged(customer => customer['id']);
    })
    .subscribe(
        customer => {
            console.log('this.isLoading = false;');
            console.log(customer);
            // this.customer = customer;
        },
        error => {
            // this.errorMessage = error;
        }
    );

See live demo: https://jsbin.com/nebutup/5/edit?js,console

The inner combineLatest() receives two Observables:

  1. The 300ms delay
  2. The customer from a remote service (in this demo simulated)

Then there's also projection function used to select what we want to propagate further. Both Observables use .startWith(null) to make make sure they have at least one item emitted so the combineLatest() will be triggered by a change in any of them. Then we can easily know whether the first Observable that emitted was the delay or the customer.

Then there's also filter() to remove all null values and distinctUntilChanged() to make sure we don't emit the same customer twice (this handles the case where the customer completes first).

Then when we run this demo and the delay is fired first the output is following:

this.isLoading = true;
this.isLoading = false;
{ name: 'John', id: 42 }

This means we first show the loading and then hide it.

Then when we change the getCustomer() to complete first:

function getCustomer(id) {
    return Observable.of({'name': 'John', id}).delay(100);
}

we'll get the following:

this.isLoading = false;
{ name: 'John', id: 42 }

This means we never show any loading.