Loading generic component

Problem & Goal

In many Angular applications, developers manually control loading indicators inside each component. While this works for small projects, it quickly becomes repetitive, error-prone, and hard to maintain as the codebase grows.

A better approach is to centralize loading state management. By using a generic loading service combined with an HTTP interceptor, we can automatically track when requests start and finish — showing a global progress spinner without writing extra code in every component.

We’ll also add a configurable flag to skip the progress spinner for certain requests — useful for silent background calls or analytics pings that don’t need to interrupt the user experience.

Our goals are:

  1. Centralized Loading State – Manage loading indicators in one place instead of scattering logic across components.
  2. Automatic Triggering – Show/hide a spinner automatically for all HTTP requests.
  3. Configurable Behavior – Allow opting out of the spinner per-request using a flag.
  4. Standalone-Friendly – Ensure the solution works seamlessly in Angular standalone projects without relying on NgModules.

Creating the Loader Component

Before we set up our HTTP interceptor, we first need a Loader Component that can display a progress spinner overlay whenever our application is performing network requests. This will serve as the visual feedback for the user, showing that something is happening in the background.

We’ll use Angular Material’s <mat-progress-spinner> because it’s lightweight, accessible, and easy to style.

Loader Template

@if (loadingService.isLoading$ | async) {
  <div class="loader-overlay">
    <mat-progress-spinner
      class="loading-spinner"
      mode="indeterminate"
      diameter="64"
    ></mat-progress-spinner>
  </div>
}

How it works:

  • We’re using Angular’s new template syntax @if to conditionally display the loader only when the isLoading$ observable emits true.
  • The mat-progress-spinner is set to indeterminate mode so it spins continuously until the request completes.
  • Wrapping it in a .loader-overlay div ensures it’s centered and blocks user interaction while active.

Loader Component Class

import { Component, inject } from '@angular/core';
import { LoadingService } from '../services/loading.service';

@Component({
  selector: 'app-loader',
  standalone: true,
  templateUrl: './loader.component.html',
  styleUrls: ['./loader.component.scss'],
})
export class LoaderComponent {
  loadingService = inject(LoadingService);
}

The component is kept intentionally simple — it just injects the LoadingService and binds to its isLoading$ observable.

Loader Styles

.loader-overlay {
  position: fixed;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  background-color: rgba(6, 27, 44, 0.2);
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.loading-spinner {
  height: 64px;
}

This styling ensures the loader:

  • Covers the entire screen.
  • Has a subtle dark overlay to focus user attention.
  • Centers the spinner both vertically and horizontally.

With the loader component ready, the next step is to create a Loading Service that will control when this spinner is shown — and later, we’ll hook it into an HTTP interceptor so it all works automatically.

Creating the Loading Service

Now that we have our loader component, we need a way to control when it appears. The LoadingService will be our central state manager for tracking ongoing HTTP requests.

Instead of just toggling a boolean, this service uses a counter (apiCount) to handle multiple simultaneous API calls. That way, the loader won’t disappear until all requests have finished.

Loading Service Code

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class LoadingService {
  // Tracks the number of ongoing API calls
  private apiCount = 0;

  private isLoadingSubject = new BehaviorSubject<boolean>(false);
  isLoading$ = this.isLoadingSubject.asObservable();

  /** Show loader (when an API starts) */
  showLoader() {
    if (this.apiCount === 0) {
      this.isLoadingSubject.next(true);
    }
    this.apiCount++;
  }

  /** Hide loader (when an API completes) */
  hideLoader() {
    if (this.apiCount > 0) {
      this.apiCount--;
    }
    if (this.apiCount === 0) {
      this.isLoadingSubject.next(false);
    }
  }

  /** Force hide the loader (e.g., when polling stops) */
  forceHideLoader() {
    this.apiCount = 0;
    this.isLoadingSubject.next(false);
  }
}

How it works

  • apiCount keeps track of the number of ongoing API calls.
  • isLoadingSubject emits true when the first API starts and false only when the last one completes.
  • showLoader() increments the counter and turns on the spinner if it’s the first request.
  • hideLoader() decrements the counter and hides the spinner when all requests are done.
  • forceHideLoader() immediately resets the counter and hides the loader — handy for scenarios like stopping background polling.

With the loader component and loading service ready, the next step is to create an HTTP interceptor that will automatically call showLoader() and hideLoader() for us — and respect a configurable flag to skip the loader for certain requests.

Creating the HTTP Interceptor

Manually calling showLoader() and hideLoader() in every component would defeat the purpose of our centralized solution. Instead, we’ll use an HTTP interceptor to hook into every request and response automatically.

This interceptor will:

  1. Call showLoader() when a request starts.
  2. Call hideLoader() when the request finishes (success or error).
  3. Respect a skip flag (X-Skip-Loader header) so certain requests won’t trigger the loader.

Interceptor Code

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { LoadingService } from '../../services/loading/loading.service';
import { finalize } from 'rxjs';

export const loaderInterceptor: HttpInterceptorFn = (req, next) => {
  const loadingService = inject(LoadingService);

  // If this request has the custom header, skip showing the loader
  const shouldSkipLoader = req.headers.has('X-Skip-Loader');

  if (!shouldSkipLoader) {
    loadingService.showLoader();
  }

  return next(req).pipe(
    finalize(() => {
      if (!shouldSkipLoader) {
        loadingService.hideLoader();
      }
    })
  );
};

How it works

  • Skip flag: By adding a custom header X-Skip-Loader to any HTTP request, we tell the interceptor not to show the spinner for that request. This is useful for silent background calls like analytics pings or cache warmups.
  • Automatic tracking: The interceptor calls showLoader() before passing the request forward, and uses finalize() to guarantee hideLoader() runs when the request completes, fails, or is canceled.
  • Standalone-friendly: This uses Angular’s HttpInterceptorFn functional API, which works seamlessly in standalone projects without the need for traditional @Injectable() class-based interceptors.

Example — Skipping the Loader for a Request

this.http.get('/api/analytics', {
  headers: { 'X-Skip-Loader': '' }
}).subscribe();

With the loader component, loading service, and interceptor in place, you now have a fully automated, configurable global loading indicator in your Angular standalone project.

In the next part, we’ll bring everything together by registering the interceptor and adding the loader component to the app so it works across all pages.

Putting It All Together

We now have:

  • A Loader Component that displays a progress spinner overlay.
  • A Loading Service that tracks ongoing HTTP requests.
  • An HTTP Interceptor that automatically triggers the loader and supports skipping it with a custom header.

Let’s integrate these pieces into our Angular standalone app.

Register the Interceptor

In a standalone Angular project, interceptors are added in the providers array of bootstrapApplication.

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { loaderInterceptor } from './app/interceptors/loader/loader.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([loaderInterceptor])),
  ],
}).catch((err) => console.error(err));

This ensures every HTTP request in your app passes through the loaderInterceptor.

Add the Loader Component to the Root Template

Place the loader component inside your AppComponent template so it can be displayed globally:

<app-loader></app-loader>
<router-outlet></router-outlet>

Since the LoaderComponent listens to LoadingService.isLoading$, it will automatically appear and disappear without any additional wiring in individual components.

Skipping the Loader for Certain Requests

For requests where you don’t want to block the UI with a spinner, simply include the X-Skip-Loader header:

this.http.get('/api/analytics', {
  headers: { 'X-Skip-Loader': '' }
}).subscribe();

This keeps the UI uninterrupted for silent background operations.

Final Thoughts

With just a few pieces — a loader component, a loading service, and a functional HTTP interceptor — we’ve created a centralized, configurable global loading indicator for an Angular standalone project.

The benefits are clear:

  • No more repetitive spinner logic in multiple components.
  • Consistent user experience for all network calls.
  • Fine-grained control to skip the loader when needed.

This pattern is clean, scalable, and fully compatible with Angular’s modern standalone architecture.