Components

A generic way of handling loading-status, saving-status and validation errors in Angular

In this article we are going to implement a generic solution on how to fix 3 common usecases that involve redundancy in CRUD applications.

A generic way of handling loading-status, saving-status and validation errors in Angular — Components article by brechtbilliet on Angular In Depth
A generic way of handling loading-status, saving-status and validation errors in Angular — Components article by brechtbilliet on Angular In Depth
On this page · 5 sections

Building Angular applications often involves repeating the same kind of logic across many components. Three typical scenarios are:

  • Indicating that data is being loaded
  • Indicating that the user is currently saving (adding, updating, or deleting data)
  • Displaying validation feedback from the server

This article walks through a reusable approach to handle these situations.

The cost of handling loading, saving, and validation in a non-generic way

Let’s first see how these needs are often addressed without a generic solution. A typical implementation — which has to be re-written for each component — looks something like this:

ngOnInit(): void {
    this.loading = true;
    const usersCompleted = false;
    const citiesCompleted = false;
    this.userService.fetch().subscribe(users => {
        usersCompleted = true;
        this.loading = usersCompleted && citiesCompleted;
    });
    this.citiesService.fetch().subscribe(cities => {
        citiesCompleted = true;
        this.loading = usersCompleted && citiesCompleted;
    });
}

You need to track which request finishes first, because you can’t flip the loading flag to false while another request is still in flight. And what if a request fails? That also has to be handled manually.

This example only covers loading two lists. The code gets unwieldy quickly, and the real drawback is that this logic needs to be duplicated in every component that performs asynchronous requests.

The situation worsens when saving data comes into play. Consider the added complexity of dealing with validation errors:

remove(user: User): void {
    // TODO: set acting to true
    this.userService.remove(user).subscribe(res => {
        // TODO: set acting to false
    });
}
update(user: User): void {
    // TODO: set acting to true
    this.userService.update(user).subscribe(res => {
        // TODO: set acting to false
        // TODO: handle validation errors
    });
}
add(user: User): void {
    // TODO: set acting to true
    this.userService.add(user).subscribe(res => {
        // TODO: set acting to false
        // TODO: handle validation errors
    });
}

To show validation errors, you have to inspect the HTTP status code for 400 and map the response data by hand.

These snippets are included to make a point clear: this repetitive and messy logic gets repeated everywhere.

Streamlining the approach

The goal can be achieved by combining an Angular service with an interceptor and Typescript decorators.

The first building block is a HttpStatusService that exposes three observables:

  • loading$: indicates whether data is being fetched
  • acting$: indicates whether the user is adding, updating, or removing data
  • validationErrors$: provides the current validation error state
// http-status.service.ts
@Injectable({
  // important to provide this service to the 
  // injector of the root module 
  providedIn: 'root'
})
export class HttpStatusService {
  // regular subject because we don't want to replay
  // the validationerrors
  private validationErrorsSub$ 
    = new Subject<ValidationError[]>();

  // 2 subjects that replays the last value 
  // (ideal for state)
  private loadingSub$ = new ReplaySubject<boolean>(1);
  private actingSub$ = new ReplaySubject<boolean>(1);

  // we don't want to expose the subject for
  // encapsulation purposes. That's why we convert them
  // into observables
  getvalidationErrors$ = 
    this.validationErrorsSub$.asObservable();
  loading$ = 
    this.loadingSub$.pipe(distinctUntilChanged());
  acting$ = 
    this.actingSub$.pipe(distinctUntilChanged());

  // these are just some regular setters to next 
  // the values in our subjects
  set validationErrors(errors: ValidationError[]) {
    this.validationErrorsSub$.next(errors);
  }

  set loading(val: boolean) {
    this.loadingSub$.next(val);
  }

  set acting(val: boolean) {
    this.actingSub$.next(val);
  }
}

This service essentially holds the state for our three statuses. The next challenge is to ensure the setters for these observables are invoked at the right moments. A manual approach for every HTTP call would undermine the purpose, so let’s build an interceptor for this task.

@Injectable({
    providedIn: 'root'
})
export class HttpStatusInterceptor implements HttpInterceptor {
  // keep track of the loading calls
  private loadingCalls = 0; 
  // keep track of the acting calls
  private actingCalls = 0; 

  constructor(
    private httpStatusService: HttpStatusService
  ) {}

  private changeStatus(val: boolean, method: string): void {
    if (['POST', 'PUT', 'DELETE', 'PATCH']
      .indexOf(method) > -1) {
      val ? this.actingCalls++ : this.actingCalls--;
      this.httpStatusService.acting = this.actingCalls > 0;
    } else if (method === 'GET') {
      val ? this.loadingCalls++ : this.loadingCalls--;
      this.httpStatusService.loading = this.loadingCalls > 0;
    }
  }
  ...
}

Here, a private changeStatus() function has been created to call the loading or acting setters on the HttpStatusService.

For HTTP methods such as POST, PUT, DELETE, or PATCH, the counter for actingCalls is incremented; if that count exceeds zero, it indicates the user is actively performing a save operation, so the acting property on the service gets set.

For GET requests, the same logic applies to the loadingCalls counter and the loading setter.

Of course, the intercept function still needs implementation. In it, every intercepted request should flip the appropriate status to true, since that signifies the start of a load or save operation. A typical interceptor works by intercepting the request, cloning it, performing some action, and returning it. The handle method on HttpHandler yields an observable, which is the perfect spot to attach a finalize operator to set the status back to false when the operation completes.

...
intercept(
  req: HttpRequest<any>,
  next: HttpHandler
): Observable<HttpEvent<any>> {
  // there is a new request, so we are definitely
  // loading or acting, we have to change the status
  this.changeStatus(true, req.method);
  return next.handle(req.clone()).pipe(
    // when the request completes, errors or times out,
    // we have to change the status as well
    finalize(() => {
      this.changeStatus(false, req.method);
    })
  );
}

This should in turn update the loading$ and acting$ observables within the HttpStatusService. But validation errors are still unaddressed. A catchError operator placed before finalize comes to the rescue:

intercept(
  req: HttpRequest<any>,
  next: HttpHandler
): Observable<HttpEvent<any>> {
  ..
  return next.handle(req.clone()).pipe(
    // catch the error
    catchError(e => {
      // if bad request > validation erors
      if (e.status === 400) { 
        // use the validationErrors setter to update
        this.httpStatusService.validationErrors = 
          e.error.validationErrors;
        // make sure that this result never 
        // reaches the component
        return NEVER;
      }
      // throw the error back
      // or put that in the `HttpStatusService` as well ;-)      
      return throwError(e); 
    }),
    finalize(...)
  );
}

Here’s the complete interceptor:

@Injectable({
    providedIn: 'root'
})
export class HttpStatusInterceptor implements HttpInterceptor {
  private loadingCalls = 0; 
  private actingCalls = 0; 

  constructor(
    private httpStatusService: HttpStatusService
  ) {}

  private changeStatus(v: boolean, method: string): void {
    if (['POST', 'PUT', 'DELETE', 'PATCH']
      .indexOf(method) > -1) {
      v ? this.actingCalls++ : this.actingCalls--;
      this.httpStatusService.acting = this.actingCalls > 0;
    } else if (method === 'GET') {
      v ? this.loadingCalls++ : this.loadingCalls--;
      this.httpStatusService.loading = this.loadingCalls > 0;
    }
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req.clone()).pipe(
      catchError(e => {
        if (e.status === 400) { 
          this.httpStatusService.validationErrors = 
            e.error.validationErrors;
          return NEVER;
        }
        return throwError(e); 
      }),
      finalize(() => {
        this.changeStatus(false, req.method);
      })
    );
  }
}

Once this interceptor is registered, any HTTP request will automatically keep the observables in HttpStatusService up-to-date without further effort.

In the root module, this interceptor needs to be declared within the providers array of the @NgModule decorator:

providers: [
  ...
  {
    provide: HTTP_INTERCEPTORS,
    multi: true,
    deps: [HttpStatusService],
    useClass: HttpStatusInterceptor
  }
]

Now components can consume the HttpStatusService directly:

export class UserComponent {
  loading$ = this.httpStatus.loading;
  validationErrors$ = this.httpStatus.validationErrors;
  acting$ = this.httpStatus.loading;

  constructor(
    private httpStatusService: HttpStatusService) {
  }
}

These three observables can be easily read in a component template via the async pipe. An example:

<my-spinner *ngIf="loading$ | async"></my-spinner>
<my-user-form 
    [validationErrors]="validationErrors$ | async"
    [disabled]="acting$ | async"></my-user-form>

A cleaner approach with decorators

We’ve already cut down on a lot of boilerplate, but decorators can make the process even smoother. Consider a refactored UserComponent:

export class UserComponent {
  @Loading()loading$;
  @ValidationErrors() validationErrors$;
  @Acting() acting$;
}

This is a highly declarative style. The component is much simpler and no longer needs to inject HttpStatusService manually.

How are these decorators made? It’s quite straightforward. A property decorator is essentially a function that returns another function receiving target and key as parameters.

export function Loading() {
  return function (target: any, key: string): void {
    // in this case the target is the component
    // instance and key the property name
    // target: userComponent, key: loading$
    // now we have to set the property to the actual 
    // loading$ observable that lives in the HttpStatusService
    target[key] = // todo
  }
};

To assign the property on the target, we need access to the HttpStatusService instance registered on the root injector — the one holding the true state. Getting that instance directly is tricky. Until Angular provides built-in support (as discussed in this GitHub issue), here’s a workaround:

In the same folder as the http-status.service file, create a new file named root-injector.ts:

import { Injector } from '@angular/core';

export let rootInjector: Injector;
export function setRootInjector(injector: Injector): void {
  rootInjector = injector;
}

The setRootInjector() function installs the injector instance into the exported rootInjector variable. The root module calls this function during setup:

@NgModule({
    ...
})
export class AppModule {
  constructor(private injector: Injector) {
    setRootInjector(injector);
  }
}

Finally, the decorators can utilize that rootInjector variable. The result:

// loading.decorator.ts
export function Loading() {
  return function (target: any, key: string): void {
    const service = rootInjector.get(HttpStatusService);
    target[key] = service.loading$;
  }
};

// acting.decorator.ts
export function Acting() {
  return function (target: any, key: string): void {
    const service = rootInjector.get(HttpStatusService);
    target[key] = service.acting$;
  }
};

// validation-errors.decorator.ts
export function ValidationErrors() {
  return function (target: any, key: string): void {
    const service = rootInjector.get(HttpStatusService);
    target[key] = service.validationErrors$;
  }
};

Conclusion

By combining an interceptor with a simple service and a handful of decorators, the repetitive code associated with loading, saving, and error states can be nearly eliminated.

Hopefully, you found this approach useful.

Special thanks

Many thanks to those who reviewed this work:

B
brechtbilliet

Writes about RxJS, Components, State. Active 2016–2022.

All 22 articles →