DEV Community

Rens Jaspers
Rens Jaspers

Posted on • Updated on

Angular Guide for Beginners: Fetching Data from an API, Handling Errors, and Efficiently Managing Loading State

In this guide, we'll learn how to get data from an API using Angular, show a loading indicator while waiting for the data, and how to display errors nicely.

Using HttpClient in Angular

To fetch data from the web, Angular provides a tool called HttpClient. To use it, you need to add it to your project. The way you add it depends on how your project is set up.

With NgModules

If your project uses NgModules, add HttpClientModule to your AppModule file:

// app.module.ts
import { HttpClientModule } from "@angular/common/http";

@NgModule({
  declarations: [
    // your components here
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    // other modules here
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

With Standalone Components

For projects using standalone components, add HttpClient in your main file where you start your application:

// main.ts
bootstrapApplication(App, { providers: [provideHttpClient()] });
Enter fullscreen mode Exit fullscreen mode

Injecting HttpClient

After adding HttpClient, you need to include it in your component by adding it to the constructor.

import { HttpClient } from '@angular/common/http';

@Component(...)
export class MyComponent {
  constructor(private http: HttpClient) {}
}
Enter fullscreen mode Exit fullscreen mode

Note: a good practice is to use HttpClient in services instead of directly in components. This makes your code neater.

Fetching Data from a Web Service

To get data from a web service, use the HttpClient.get() method. When you call this method, you can handle the data received or any errors that occur.

@Component(...)
export class MyComponent implements OnInit {
  data: any;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http.get('https://api.mywebsite.com/data').subscribe((response) => {
      this.data = response;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Showing a Loading Indicator

It's a good idea to show a loading spinner or icon while waiting for the data. You can do this by using an isLoading variable that is true when the data is being fetched and false once it's loaded. You use this variable in your template to show or hide the loading icon.

@Component(...)
export class MyComponent implements OnInit {
  data: any;
  isLoading: boolean;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.isLoading = true;
    this.http.get('https://api.mywebsite.com/data').subscribe((response) => {
      this.data = response;
      this.isLoading = false;
    }, (error) => {
      this.isLoading = false;
      console.log('Error:', error);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, use the isLoading property to conditionally display a loading spinner in your component's template:

@if (isLoading) {
  <!-- Display your loading spinner or skeleton here -->
  Loading...
} @else {
  <!-- Display your data here -->
  {{ data }}
}
Enter fullscreen mode Exit fullscreen mode

Handling Errors

If there's a problem fetching the data, you should show an error message. You can add an error variable that stores error information and use it in your template to show an error message when needed:

@Component(...)
export class MyComponent implements OnInit {
  data: any;
  isLoading: boolean;
  error: string;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.isLoading = true;
    this.http.get('https://api.mywebsite.com/data').subscribe((response) => {
      this.data = response;
      this.isLoading = false;
    }, (error) => {
      this.isLoading = false;
      this.error = 'An error occurred while fetching data';
      console.log('Error:', error);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

And in your template:

@if(isLoading {
  Loading...
} @else {
  @if(error) {
    {{ error.message }}
  } @else {
    {{ data }}
  }
}
Enter fullscreen mode Exit fullscreen mode

Having completed these steps, you now possess a basic setup for fetching data from an API in Angular, showing a loading spinner during data fetching, and handling errors.

Improving Data Fetching with the async Pipe

Angular has a feature called the async pipe that can make working with data from web services easier. It automatically handles subscribing and unsubscribing to data streams, which helps prevent memory leaks.

This is a pattern for using the async pipe you will see often:

@Component(...)
export class MyComponent {
  data$ = this.http.get('https://api.mywebsite.com/data');
}
Enter fullscreen mode Exit fullscreen mode
@if(data$ | async; as data) {
  {{ data }}
} @ else {
  Loading...
}
Enter fullscreen mode Exit fullscreen mode

Besides preventing memory leaks this approach also makes sure we don’t introduce side effects like setting an isLoading flag and storing the data in the data property of our components.

This is a big deal because effects make your code harder to understand and harder to debug and test: any part of the application can edit the isLoading or data property.

But be careful! Using the async pipe with raw data has major drawbacks such as incorrectly showing a loading icon when the data is actually loaded but is a falsy value (like 0 or null) or not handling errors properly.

A Better Approach: Loading State Object Stream

A better way to handle data loading and errors while using the async pipe is to create a special object that clearly indicates the loading state. This object can have a state property that shows whether the data is loading, loaded, or there was an error.

This is what a loading state object could look like:

interface Loading {
  state: "loading";
}

interface Loaded<T> {
  state: "loaded";
  data: T;
}

interface Errored {
  state: "error";
  error: Error;
}

type LoadingState<T = unknown> = Loading | Loaded | Errored;
Enter fullscreen mode Exit fullscreen mode

In your component, you can use it like this:

@Component()
export class MyComponent {
  data$ = this.http.get('https://api.mywebsite.com/data').pipe(
    map(data => ({ state: "loaded", data })),
    catchError(error => of({ state: "error", error })),
    startWith({state: "loading"})
  );
Enter fullscreen mode Exit fullscreen mode

When the data comes in, it is piped through the map operator to create a Loaded object. If there's an error, it's caught by the catchError operator and turned into an Errored object. The startWith operator is used to emit a Loading object when the stream starts.

In your template, you can use @switch to easily handle all the loading outcomes:

@if (data$ | async; as data) {
  @switch (data.state) {
    @case ("loading") {
      Loading...
    }
    @case ("error") {
      Error: {{ data.error.message }}
    }
    @case ("loaded") {
      {{ data.data }}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This method is more clear and avoids the issues of directly using the data stream with the async pipe. It makes your code easier to understand and manage.

If you think the code in the last part is a bit hard, you're not alone. It includes some advanced topics from TypeScript and RxJS. But, you can easily make a function that changes your data stream into a loading state stream.

Here's how you can make any data stream show loading, loaded, or error states:

// to-loading-state-stream.ts
import { Observable, of } from "rxjs";
import { catchError, map, startWith } from "rxjs/operators";
import { LoadingState } from "./loading-state.interface";

export function toLoadingStateStream<T>(
  source$: Observable<T>,
): Observable<LoadingState<T>> {
  return source$.pipe(
    map((data) => ({ state: "loaded", data })),
    catchError((error) => of({ state: "error", error })),
    startWith({ state: "loading" }),
  );
}
Enter fullscreen mode Exit fullscreen mode

You can import this function and use it in any component:

@Component()
export class MyComponent {
  data$ = toLoadingStateStream(this.http.get("https://api.mywebsite.com/data"));
}
Enter fullscreen mode Exit fullscreen mode

If you still find this too tricky, I suggest checking out my library called ngx-load-with. It makes loading data very simple, even if you're not familiar with RxJS. It's as powerful and safe as the method we talked about but much easier to use. Check it out at github.com/rensjaspers/ngx-load-with and give it a star if it helps you!

Top comments (0)