DEV Community

Cover image for Keeping up with http state in Angular
M. Andrew Darts
M. Andrew Darts

Posted on

Keeping up with http state in Angular

Keeping up with the state of HTTP requests can be difficult and feel like too much work sometimes. Is it fetching for the first time? Is it fetching more? Is there an error? To give the user a nice experience, we should think about these things. Knowing if we are in the initial request for data can tell us to show a specific loading screen like skeleton screens. Loading more data could tell us we just need to show a small loading indicator. Knowing we have an error can tell us we need to let the user try again. Preparing for these different cases really makes it easier for us. Creating a standard structure for these states will also help us in keeping it consistent across our app. We are going to look at a simple example you could use to know the state of your requests and keep it consistent using RxJS in Angular. By creating a reusable pipe, we can reuse this across any requests. Keeping it in one place will also help add new states in the future without updating all instances.

Define the state of our request

First, let's think about which states we want to track during our request. We will start by tracking the loading and error states. We can create an interface for this. We shouldn't forget the data we are loading, so we will add a data property. The interface will look similar to this:

interface HttpState {
  loading: boolean;
  error: Error | null;
  data: any;
}
Enter fullscreen mode Exit fullscreen mode

This looks good, but we do want the ability to specify a type for our data. We can achieve this by passing a type into the interface.

interface HttpState<T> {
  loading: boolean;
  error: Error | null;
  data: T;
}
Enter fullscreen mode Exit fullscreen mode

If you are not familiar with this syntax, think of HttpState as a function and <T> as an argument. We can pass in a type to set data. If you want to take a closer look at this, check out https://www.youtube.com/watch?v=dLPgQRbVquo. This guy is a TypeScript wizard!

Now that we have the structure of our HTTP state, let's create an observable that follows. We are going to fetch some Pokémon from https://pokeapi.co/api/v2/pokemon.

Creating our observable

The simplest approach we could take is creating an observable that returns the structure of our interface.

this.http.get('https://pokeapi.co/api/v2/pokemon')
  .pipe(
    map((data) => ({ loading: false, data, error: null }))
  )
Enter fullscreen mode Exit fullscreen mode

Now we are fetching our data and mapping it to our HttpState interface. There is one issue: we won't have any state until our request is finished. We also need our loading property to start off as true. RxJS has an operator just for this called startWith.

this.http.get('https://pokeapi.co/api/v2/pokemon')
  .pipe(
    map((data) => ({ loading: false, data, error: null })),
    startWith({loading: true, error: null, data: null}),
  )
Enter fullscreen mode Exit fullscreen mode

Using the startWith operator will give us an initial value for our HttpState. Before the request finishes, our state will be {loading: true, data: null, error: null}. After the request finishes, our state will be {loading: false, data: ...data, error: null}. This is exactly what we want. There is one last thing: we need to handle the errors. Believe it or not, Rxjs has another operator to suit our needs. The catchError operator will be called if an error occurs in the observable. Using the catchError operator, we will be able to handle the error and return a new observable. Returning the new observable will give us the ability to continue without breaking the observable.

this.http.get('https://pokeapi.co/api/v2/pokemon')
  .pipe(
    map((data: T) => ({ loading: false, data, error: null })),
    catchError((error) => of({ loading: false, error, data: null })),
    startWith({loading: true, error: null, data: null}),
  )
Enter fullscreen mode Exit fullscreen mode

Let's run through this line we added. We are using the catchError pipe to catch any errors in the request. The coolest part of the catchError pipe is that we can return a new observable so we don't break our stream. We are using of to create a new observable with an object that follows our HttpState interface. All we need to do is set our error property to the error that was thrown.

Putting it all together

Let's put this to use. We will be fetching some Pokémon from the https://pokeapi.co/ API and creating a component to show the different states of our request.

@Component({
  selector: 'app-pokemon',
  template: `
    <ng-container *ngIf="results$ | async as results">
      <div *ngIf="results.loading">Loading...</div>

       <div *ngIf="results.error">{{ results.error.message }}</div>

       <ul *ngIf="!results.loading && !results.error">
         <li *ngFor="let pokemon of results.data">{{ pokemon.name }}</li>
       </ul>
    </ng-container>
  `,
})
export class PokemonComponent {
    results$ = this.http.get<Pokedex>('https://pokeapi.co/api/v2/pokemon')
    .pipe(
        map((data: T) => ({ loading: false, data, error: null })),
        catchError((error) => of({ loading: false, error, data: null })),
        startWith({loading: true, error: null, data: null}),
    )

  constructor(private http: HttpClient) {}
}

export interface Pokedex {
    count:    number;
    next:     string;
    previous: null;
    results:  Result[];
}

export interface Result {
    name: string;
    url:  string;
}
Enter fullscreen mode Exit fullscreen mode

In our template, we are accessing the HTTP state to display in the UI.

Telling the user we are loading some data.

<div *ngIf="results.loading">Loading...</div>
Enter fullscreen mode Exit fullscreen mode

We can show our error message.

<div *ngIf="results.error">{{ results.error.message }}</div>
Enter fullscreen mode Exit fullscreen mode

Display our results.

<ul *ngIf="!results.loading && !results.error">
  <li *ngFor="let pokemon of results.data">{{ pokemon.name }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

One step further

Since we want to use HttpState in other parts of our app, we don't want to write the map, catchError, and startWith pipes every time. Pipes in RxJS are a way to contain manipulations we want to apply to observables. Let's create one operator we can use anywhere we want to implement this HTTP state pattern. A pipe in RxJS is just a function that returns an observable. We don't want to create a new observable; we simply want to add some operators to the existing observable.

export function withHttpState<T>() {
  return (o: Observable<any>): Observable<HttpState<T>> =>
    o.pipe(
      map((data: T) => ({ loading: false, data })),
      catchError((error) => of({ loading: false, error })),
      startWith({ loading: true })
    );
}

this.http.get<Pokedex>('https://pokeapi.co/api/v2/pokemon')
    .pipe(withHttpState<Pokedex>())
Enter fullscreen mode Exit fullscreen mode

Conclusion

UI is important. HTTP requests can sometimes be difficult to keep up with. Creating a consistent mechanism to track the state is always a win. In just a few lines, we can keep up with the state of our requests so we can reflect it in the UI or handle it as we see fit. RxJS is a powerful library but can be very intimidating at times. Hopefully, this example will make some things clearer and spark some ideas for different ways of using it to solve issues in your projects.

Thanks for reading!

Top comments (0)