Search code examples
javascriptangulartypescriptspinnerangular-http-interceptors

delayed Angular spinner without flickering


At the moment I'm trying to use the Angular HttpInterceptor to show a spinner once, so it doesnt flicker everytime a HttpRequest is done. My currect code for the interceptor.service.ts looks like this:

@Injectable()
export class InterceptorService implements HttpInterceptor {
  constructor(private spinnerService: SpinnerService) {}

  showSpinner() {
    this.spinnerService.show.next(true);
    console.log('true');
  }

  hideSpinner() {
    this.spinnerService.show.next(false);
    console.log('false');
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler,
  ): Observable<HttpEvent<any>> {
    const responseTimer$ = next
      .handle(req)
      .pipe(filter(e => e instanceof HttpResponse));
    timer(300)
      .pipe(takeUntil(responseTimer$))
      .subscribe(() => this.showSpinner());

    return next.handle(req).pipe(
      tap(evt => {
        if (evt instanceof HttpResponse) {
          this.hideSpinner();
        }
      }),
    );
  }
}

But if i try this code, the console says:

interceptor.service.ts:20 true
interceptor.service.ts:25 false
4 x interceptor.service.ts:20 true
4 x interceptor.service.ts:25 false
5 x interceptor.service.ts:25 false
interceptor.service.ts:25 false

But in my opinion it should be x times true and at the end false. I hope you understand what i'm trying to archieve.


Solution

  • It's easier to create your own Subject that will track how many requests have been made, and then safeguard yourself from mistakes by throwing an error if the tracking count goes below zero.

    class BusySubject extends Subject<number> {
        private _previous: number = 0;
    
        public next(value: number) {
            this._previous += value;
            if (this._previous < 0) {
                throw new Error('unexpected negative value');
            }
            return super.next(this._previous);
        }
    }
    

    The above is a subject that will sum the values overtime with a safety check for below zero.

    You can then use this in an interceptor

    @Injectable()
    export class BusyInterceptor implements HttpInterceptor {
    
        private _requests: BusySubject = new BusySubject();
    
        public get busy(): Observable<boolean> {
            return this._requests.pipe(
                map((requests: number) => Boolean(requests)),
                distinctUntilChanged(),
            );
        }
    
        public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
            this._requests.next(1);
            return next.handle(req).pipe(finalize(() => this._requests.next(-1)));
        }
    }
    

    It's important to noticed in the above that I use distinctUntilChanged() for the busy indicator, and also use finalize() for the decrease trigger. These are important to track the proper state of being busy.

    The above should work no matter when you subscribe to busy. Since the _previous value is an internal state of the subject. It should continue to track correctly even if nothing is subscribed. It works kind of like a BehaviorSubject() except that it's using a += to apply the changes.