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 {}
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()] });
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) {}
}
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;
});
}
}
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);
});
}
}
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 }}
}
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);
});
}
}
And in your template:
@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:
@Component(...)
export class MyComponent {
data$ = this.http.get('https://api.mywebsite.com/data');
}
@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:
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:
@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:
@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:
// 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:
@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 and give it a star if it helps you!
Top comments (1)
Thank you Rens for the clear examples!
There is one issue, your error message is set as a string instead of an object.
I believe it should look like this (or like a new Error object):
this.error = { message: 'An error occurred while fetching data' };