How to Use an Interceptor in Angular with Signals and Standalone Components to Display a Dynamic Loader

With the introduction of Angular 17 and new features like Signals and Standalone Components, state management and application structure have become simpler and more reactive. In this guide, I will show you how to implement an HTTP interceptor to display a dynamic loader during HTTP calls, leveraging
Signals for state management and Standalone Components for a leaner structure.

1. Create a Loader Service with Signals

Let’s start by creating a service that uses Signals to manage the loader’s state.

Step 1: Generate the Service

Run the following command:


    ng generate service loader
  

Step 2: Implement the Service with Signals

In the loader.service.ts file, use signals to manage the loader state:

  

  import { Injectable, signal } from '@angular/core';
  
  @Injectable({
providedIn: 'root'
})
export class LoaderService {
isLoading = signal<boolean>(false);

show(): void {
this.isLoading.set(true);
}

hide(): void {
this.isLoading.set(false);
}
}
  
  

2. Create a Standalone Component for the Loader

Now, create a Standalone Component that displays the loader when isLoading is true.

Step 1: Generate the Component

Run the following command:


    ng generate component loader --standalone
  

Step 2: Implement the Component

In the loader.component.ts file, import the service and use isLoading to control the loader’s visibility:

  

import { Component, inject } from '@angular/core';
import { LoaderService } from './loader.service';
import { ProgressSpinnerModule } from 'primeng/progressspinner';

@Component({
selector: 'app-loader',
standalone: true,
imports: [ProgressSpinnerModule],
template: `
@if (loaderService.isLoading()) {
<div class="loader-overlay">
<div class="loader-spinner">
<p-progressSpinner></p-progressSpinner>
</div>
</div>
}
`,
styles: [`
.loader-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;

height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.loader-spinner {
color: white;
}
`]
})
export class LoaderComponent {
loaderService = inject(LoaderService);
}
  
  

3. Create the HTTP Interceptor

Now, let’s create the interceptor that will intercept all HTTP calls and update the loader state.

Step 1: Generate the Interceptor

Run the following command:


    ng generate interceptor loader
  

Step 2: Implement the Interceptor

In the loader.interceptor.ts file, implement the logic to show and hide the loader:

  

import { Injectable, inject } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor

} from '@angular/common/http';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoaderService } from './loader.service';

@Injectable()
export class LoaderInterceptor implements HttpInterceptor {
private loaderService = inject(LoaderService);

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Mostra il loader
this.loaderService.show();

return next.handle(request).pipe(
finalize(() => {
// Nasconde il loader quando la richiesta è completata
this.loaderService.hide();
})
);
}
}
  
  

4. Configure the Interceptor in a Standalone Application

With Angular 17 and Standalone Components, the interceptor configuration is done directly in the main.ts or app.config.ts file.

Step 1: Configure the Interceptor in app.config.ts

Create or modify the app.config.ts file to include the interceptor:

  

import { ApplicationConfig, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { LoaderInterceptor } from './loader.interceptor';

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: LoaderInterceptor, multi: true }
]
};
  
  

5. Include the Loader Component in the App

Now, include the LoaderComponent in the main application template.

Step 1: Add the Component in app.component.ts

Make sure app.component.ts is a Standalone Component and imports LoaderComponent:

  

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoaderComponent } from './loader/loader.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, LoaderComponent],
template: `
<app-loader></app-loader>
<router-outlet></router-outlet>
`,
})
export class AppComponent {}
  
  

6. Test the Implementation

Now, whenever an HTTP call is made, the loader will automatically appear until the request completes.

Example of an HTTP Call

Here is an example service that makes an HTTP call:

  

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class DataService {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com/posts';

getPosts(): Observable<any> {
return this.http.get(this.apiUrl);
}
}
  
  

When you call getPosts() from a component, the loader will automatically appear and disappear once the request is finished.

7. Optimizations and Considerations

Exclude Certain Requests

If you don’t want the loader to appear for some requests (e.g., polling calls or background requests), you can modify the interceptor to ignore them:

  

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Ignora le richieste con un header specifico
if (request.headers.get('skip-loader')) {
return next.handle(request);
}

this.loaderService.show();
return next.handle(request).pipe(
finalize(() => {
this.loaderService.hide();
})
);
}
  
  

Error Handling

If you want to ensure that the loader is hidden even in case of an error, you can use the catchError operator:

  

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

// ...
return next.handle(request).pipe(
catchError(error => {
this.loaderService.hide();
return throwError(() => error);
}),
finalize(() => {
this.loaderService.hide();
})
);
  
  

Conclusion

With Angular 17, Signals, and Standalone Components, managing a dynamic loader during HTTP calls becomes simpler and more reactive. By using an HTTP interceptor and a service based on Signals, you can:

  • Centralize the loader display logic.
  • Avoid code duplication.
  • Improve user experience with immediate visual feedback.

This solution is modern, efficient, and scalable,perfect for Angular applications aiming for maximum reactivity and usability.

Blog

Other articles you might be interested in