Angular Guide for Beginners: Fetching Data from an API, Handling Errors, and Efficiently Managing Loading State
Tags: angular, beginners
Published: 2023-06-08 06:55:33

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:

```typescript
// 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 {}
```

#### With Standalone Components

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

```typescript
// main.ts
bootstrapApplication(App, { providers: [provideHttpClient()] });
```

#### Injecting HttpClient

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

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

@Component(...)
export class MyComponent {
  constructor(private http: HttpClient) {}
}
```

> 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.

```typescript
@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;
    });
  }
}
```

### 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.

```typescript
@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);
    });
  }
}
```

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

```angular
@if (isLoading) {
  <!-- Display your loading spinner or skeleton here -->
  Loading...
} @else {
  <!-- Display your data here -->
  {{ data }}
}
```

### 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:

```typescript
@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);
    });
  }
}
```

And in your template:

```angular
@if(isLoading {
  Loading...
} @else {
  @if(error) {
    {{ error.message }}
  } @else {
    {{ data }}
  }
}
```

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:

```typescript
@Component(...)
export class MyComponent {
  data$ = this.http.get('https://api.mywebsite.com/data');
}
```

```angular
@if(data$ | async; as data) {
  {{ data }}
} @ else {
  Loading...
}
```

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:

```typescript
interface Loading {
  state: "loading";
}

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

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

type LoadingState<T = unknown> = Loading | Loaded | Errored;
```

In your component, you can use it like this:

```typescript
@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"})
  );
```

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:

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

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:

```typescript
// 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" }),
  );
}
```

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

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

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](https://github.com/rensjaspers/ngx-load-with) and give it a star if it helps you!