DEV Community

Cory Rylan
Cory Rylan

Posted on • Updated on • Originally published at coryrylan.com

Angular HTTP Pending Request Pattern

Often in single-page apps, we want to show the status of when something is loading as well as show the user when something goes wrong. Pending states can be rather tricky when dealing with asynchronous JavaScript. In Angular, we have RxJS Observables to help us manage async complexity. This post, I'll show a pattern I came up with to solve something I was working on that helped me display the status of an API request as well as any errors.

This pattern I call the pending request pattern. Probably not a great name, but here is how it works. Typically we make an HTTP request in Angular and get back a single Observable that will emit the request value when completed. We have no easy way to show the user that we are loading the data or when something goes wrong without a lot of code defined within our components. With this pattern, instead of the service returning the response Observable, we return a new object that contains two Observables. One Observable for the HTTP response and another Observable with status updates of the request.

export interface Pending<T> {
  data: Observable<T>;
  status: Observable<Status>;
}

export enum Status {
  LOADING = 'LOADING',
  SUCCESS = 'SUCCESS',
  ERROR = 'ERROR'
}
Enter fullscreen mode Exit fullscreen mode

In our services, we can return the pending object instead of the raw HTTP Observable.

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  load(userId: number): Pending<User> { ... }
}
Enter fullscreen mode Exit fullscreen mode

Within our load method we can push out status updates to anyone using our Pending object. This pattern makes our code cleaner within our components. Let's take a look at a component example, and we will come back to the load() implementation.

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

import { UserService, User, Pending, Status } from './user.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent {
  readonly Status = Status;
  readonly user: Pending<User>;

  constructor(private userService: UserService) {
    this.user = this.userService.load(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the component, we inject the UserService to call the load() method and assign the Pending object to the user property. I also set the Status enum as a property of the class so I can reference it within my template.

<section *ngIf="user.data | async as user">
  <h3>{{user.name}}</h3>
  <p>Height: {{user.height}}</p>
  <p>Mass: {{user.mass}}</p>
  <p>Homeworld: {{user.homeworld}}</p>
</section>
Enter fullscreen mode Exit fullscreen mode

In the template, we use the async pipe to subscribe to my user data from the Pending object. Once subscribed, we can display my data as usual per an Angular template.

To display status messages, we can subscribe to the status Observable within the template as well.

<section *ngIf="user.data | async as user">
  <h3>{{user.name}}</h3>
  <p>Height: {{user.height}}</p>
  <p>Mass: {{user.mass}}</p>
  <p>Homeworld: {{user.homeworld}}</p>
</section>

<section [ngSwitch]="user.status | async">
  <span *ngSwitchCase="Status.LOADING">Loading User...</span>
  <span *ngSwitchCase="Status.ERROR">There was an error loading the user.</span>
</section>
Enter fullscreen mode Exit fullscreen mode

Now when the user status returns an update that the request has started, we will show the Loading User... message. When the status emits an update that there was any error, we can show the error message to the user. With the Pending object, we can show the status of a current request within our template pretty easily.

Now we go back to our UserService we can see how we implemented the Pending object within the load() method.

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

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  load(userId: number): Pending<User> {
    const status = new ReplaySubject<Status>();
    const data = this.http.get<User>(`https://swapi.co/api/people/${userId}`);

    return { data, status };
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is our starting point for the load() method of our service. We have two Observables making up our pending object. First is the status which is a special kind of Observable called a ReplaySubject.

The ReplaySubject will allow anyone who subscribes after events have already fired to get the last event that was emitted. Second is our standard HTTP Observable from the Angular HTTP Client Service.

First, we want to be able to notify when the request has started. To do this we need to wrap our HTTP Observable so we can emit a new status when subscribed to.

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

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  load(userId: number): Pending<User> {
    const status = new ReplaySubject<Status>();
    const request = this.http.get<User>(`https://swapi.co/api/people/${userId}`);

    const data = defer(() => {
      status.next(Status.LOADING);
      return request;
    });

    return { data, status };
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the defer function from RxJS, we can wrap the existing HTTP
Observable, execute some code, and then return the new Observable. By using the defer, we can trigger a status loading event only when someone subscribes. Using the defer is important because Observables, by default, are lazy and won't execute our HTTP request until subscribed.

Next we need to handle errors if something goes wrong with our request.

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  load(userId: number): Pending<User> {
  const status = new ReplaySubject<Status>();

  const request = this.http
    .get<User>(`https://swapi.co/api/people/${userId}`)
    .pipe(
      retry(2),
      catchError(error => {
      status.next(Status.ERROR);
      throw 'error loading user';
     })
    );

   const data = defer(() => {
     status.next(Status.LOADING);
     return request;
   });

   return { data, status };
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the catchError and retry operators, we can catch exceptions and retry a given number of failures. Once we have our starting and error status events working, we need to add a success status.

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  load(userId: number): Pending<User> {
    const status = new ReplaySubject<Status>();

    const request = this.http
      .get<User>(`https://swapi.co/api/people/${userId}`)
      .pipe(
        retry(2),
        catchError(error => {
          status.next(Status.ERROR);
          throw 'error loading user';
        }),
        tap(() => status.next(Status.SUCCESS))
      );

      const data = defer(() => {
        status.next(Status.LOADING);
        return request;
      });

    return { data, status };
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the tap operator, we can trigger a side effect (code outside the observable) function whenever an event returns from the HTTP Observable. With tap, trigger a success status event to our component.

There is a feature within the Angular HTTP Client Service
which can accomplish something similar by listening to HTTP event updates from a request. However, this can be expensive as it causes change detection to run with every event.

Let me know in the comments below what you think! This pattern solved a few scenarios for me, but I'm interested in expanding more on this.Demo Example

Top comments (3)

Collapse
 
johncarroll profile image
John Carroll

Hmm. This seems like more boilerplate than is needed to determine if something is loading. The HttpClient is a great example, because a request is loading while the request observable is incomplete.

In the case of the HttpClient, I think a simpler solution is a service which simply monitors the HttpClient request and returns true or false depending on whether the request had completed or not. Then in the template, you could subscribe to that service to see if the request was loading.

For example, when loading a user you could say

const request = this.http.get<User>(`https://swapi.co/api/people/${userId}`);

isLoading.add(request, { key: "user-loading" })
Enter fullscreen mode Exit fullscreen mode

And then in the template you could have something like

<section *ngIf="request | async as user">
  <h3>{{user.name}}</h3>
  <p>Height: {{user.height}}</p>
  <p>Mass: {{user.mass}}</p>
  <p>Homeworld: {{user.homeworld}}</p>
</section>

<section *ngIf="isLoading.loading('user-loading') | async">
  <span>Loading User...</span>
</section>
Enter fullscreen mode Exit fullscreen mode

Of course, you could get fancier and introduce custom pipes or directives to help yourself out, but the general concept is simple

<section *ngIf="'user-loading' | isLoadingPipe | async">
  <span>Loading User...</span>
</section>
Enter fullscreen mode Exit fullscreen mode

Handling errors wouldn't be proscribed in any way. For example, someone could simply return a special request object with a success property and any relevant data.

Unsurprisingly, I wrote a post on this strategy.

It also adds more flexibility to the user, because sometimes "loading" can come from multiple sources. In my app for example, I have a form component that is dynamically, and lazily, built from multiple sources. The form itself doesn't have any particular knowledge of it's children and the children don't have knowledge of each other. How then, to tell the form when it has finished loading? Well in this case, each child can simply add loading indicators keyed to the form's ID.

For example:

ngOnInit() {
  const fetchFormChildData = this.getData();

  this.isLoading.add(fetchFormChildData, {
    key: this.formId,
  })
}
Enter fullscreen mode Exit fullscreen mode

Here, isLoading.loading(this.formId) will resolve to true so long as any children are loading. Anyway, you can see more in the post.

Collapse
 
coryrylan profile image
Cory Rylan • Edited

I like your custom pipe strategy! Very nice post.

I ran into this pattern mostly because of the error handling. I could handle errors at the service level and then use a toast UI pattern or some kind of global level UI to display error but this app needed errors inline in multiple places within the UI. Because of this I was trying to find a way to reduce the amount of logic within the templates/components for error/retry messages.

In the app that I tried this on I had a generic service that abstracted away the boiler plate code you mentioned which helps some. I'm planing on writing up a small follow up post or add on to this that shows an example of making it more generic.

Collapse
 
johncarroll profile image
John Carroll

I see the advantage to normalizing http error handling as you've described, but I'm still skeptical that this approach to loading is generally applicable. It doesn't strike me as generally helpful to link the loading status to the error status (though it certainly might be in specific contexts), but I'll look for your follow up post 👍.

There are many times when I want to indicate something is "loading", without creating a specific error handler for that thing. For example, if I was lazy loading a component, I might want to indicate that something was loading while relying on a global error handler to trigger if the client happened to have lost their internet connection.