Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJS: show loading if request is slow

I thought of using RxJS to solve elegantly this problem, but after trying various approaches, I couldn't find out how to do it...

My need is quite common: I do a Rest call, ie. I have a Promise. If the response comes quickly, I just want to use the result. If it is slow to come, I want to display a spinner, until the request completes. This is to avoid a flash of a the spinner, then the data.

Maybe it can be done by making two observables: one with the promise, the other with a timeout and showing the spinner as side effect. I tried switch() without much success, perhaps because the other observable doesn't produce a value.

Has somebody implemented something like that?

like image 881
PhiLho Avatar asked Oct 28 '25 08:10

PhiLho


2 Answers

Based on @PhiLho's answer, I wrote a pipeable operator, which does exactly that:

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                undefined,
                () => {
                    timerSub.unsubscribe();
                }
            )
        );
    }
}

Basically it returns a function, which gets the Observable source.
Then it starts a timer, using the given delay.
If this timer emits a next-event, the function is called.
However, if the source emits a next, the timer is cancelled and a new one is startet.
In the complete of the source, the timer is finally cancelled. This operator can then be used like this:

this.loadResults().pipe(
    executeDelayed(
        () => this.startLoading(),
        500
    )
).subscribe(results => this.showResult())

I did not wirte many operators myself, so this operator-implementation might not be the best, but it works.
Any suggestions on how to optimize it are welcome :)

EDIT: As @DauleDK mentioned, a error won't stop the timer in this case and the fn will be called after delay. If thats not what you want, you need to add an onError-callback in the tap, which calls timerSub.unsubscribe():

export function executeDelayed<T>(
    fn : () => void,
    delay : number,
    thisArg? : any
) : OperatorFunction<T, T> {
    return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
        let timerSub = timer(delay).subscribe(() => fn());
        return source.pipe(
            tap(
                () => {
                    timerSub.unsubscribe();
                    timerSub = timer(delay).subscribe(() => fn());
                },
                () => timerSub.unsubscribe(),   // unsubscribe on error
                () => timerSub.unsubscribe()
            )
        );
    }
}
like image 185
Robert P Avatar answered Oct 31 '25 04:10

Robert P


Here is an example that I have used. We assume here that you get the data that you want to send to the server as an Observable as well, called query$. A query coming in will then trigger the loadResults function, which should return a promise and puts the result in the results$ observable.

Now the trick is to use observable$.map(() => new Date()) to get the timestamp of the last emitted value.

Then we can compare the timestamps of the last query and the last response that came in from the server.

Since you also wanted to not only show a loading animation, but wanted to wait for 750ms before showing the animation, we introduce the delayed timestamp. See the comments below for a bit more explanation.

At the end we have the isLoading$ Observable that contains true or false. Subscribe to it, to get notified when to show/hide the loading animation.

const query$ = ... // From user input.

const WAIT_BEFORE_SHOW_LOADING = 750;

const results$ = query$.flatMapLatest(loadResults);

const queryTimestamp$ = query$.map(() => new Date());
const resultsTimestamp$ = results$.map(() => new Date());
const queryDelayTimestamp$ = (
    // For every query coming in, we wait 750ms, then create a timestamp.
    query$
    .delay(WAIT_BEFORE_SHOW_LOADING)
    .map(() => new Date())
);

const isLoading$ = (
    queryTimestamp$.combineLatest(
        resultsTimestamp$,
        queryDelayTimestamp$,
        (queryTimestamp, resultsTimestamp, delayTimestamp) => {
            return (
                // If the latest query is more recent than the latest
                // results we got we can assume that
                // it's still loading.
                queryTimestamp > resultsTimestamp &&
                // But only show the isLoading animation when delay has passed
                // as well.
                delayTimestamp > resultsTimestamp
            );
        }
    )
    .startWith(false)
    .distinctUntilChanged()
);
like image 40
Gregor Müllegger Avatar answered Oct 31 '25 02:10

Gregor Müllegger



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!