Search code examples
angularangular-routerangular-router-guards

How to display loading indicator without flickering on redirection


I'm currently trying to display a loading indicator on navigation in Angular. The issue I am experiencing is that when the user is tries to get on a protected route, the guard will redirect them but that means my loading service turns on/off/on/off, because the navigation gets cancelled and started again. How would you approach this issue?

Here's my flow exactly with examples: LoadingService

export class LoadingService {
  private readonly loadingSubject = new BehaviorSubject<boolean>(false);
  public readonly loading$ = this.loadingSubject.asObservable();

  private timeoutId: ReturnType<typeof setTimeout> | null = null;

  setLoading(isLoading: boolean, delay = 200): void {
    if (isLoading) {
      // Introduce a delay before showing the spinner
      this.timeoutId = setTimeout(() => {
        this.loadingSubject.next(true);
      }, delay);
    } else {
      // Clear the timeout if navigation completes quickly
      if (this.timeoutId !== null) {
        clearTimeout(this.timeoutId);
        this.timeoutId = null;
      }
      this.loadingSubject.next(false);
    }
  }
}

After authentication the loading service sets loading on true and routes to the correct url

    this.authService
      .login(email, password)
      .pipe(take(1))
      .subscribe({
        next: () => {
          const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/devices';
          this.loadingService.setLoading(true, 0);
          this.router.navigate([returnUrl]);
        },

in my app component:

    this.router.events
      .pipe(takeUntilDestroyed(this._destroyRef))
      .subscribe((event) => {
        if (event instanceof NavigationStart) {
          this._loadingService.setLoading(true);
        } else if (
          event instanceof NavigationEnd ||
          event instanceof NavigationCancel ||
          event instanceof NavigationError
        ) {
          this._loadingService.setLoading(false);
        }
    });

Solution

  • Sounds like you have a specific edge case you want to handle.

    I would suggest using the rxjs operator pairwise to get the previous and current navigation state.

    Using this, you can filter out your specific scenario and prevent the flickering from happening.

    this.router.events
      .pipe(takeUntilDestroyed(this._destroyRef), pairwise())
      .subscribe(([prevEvent, currEvent]: any) => {
        // below if condition specifically for handling edgecases:
        if(prevEvent instanceof NavigationCancelled &&
           currEvent instanceof NavigationStart) {
          // set loading to false, or event better do not change the state.
          this._loadingService.setLoading(false);
          // do not execute the bottom code, just exit.
          return;
        }
        if (currEvent instanceof NavigationStart) {
          this._loadingService.setLoading(true);
        } else if (
          currEvent instanceof NavigationEnd ||
          currEvent instanceof NavigationCancel ||
          currEvent instanceof NavigationError
        ) {
          this._loadingService.setLoading(false);
        }
    });