In this tutorial, we'll explore how to retrieve data from an API using Angular, manage the loading state, and show a loading spinner to enhance user experience during data fetch operations. We'll also delve into error handling and how to visually represent errors in the user interface.
Importing and Injecting HttpClient
To start, we'll need to make use of the HttpClient module, an integral part of Angular's common HTTP package that enables HTTP requests. First, import the module into your root module, typically AppModule
:
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
// your components here
],
imports: [
BrowserModule,
HttpClientModule
// other modules here
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Next, inject the HttpClient
into your component as follows:
import { HttpClient } from '@angular/common/http';
@Component(...)
export class MyComponent {
constructor(private http: HttpClient) {}
}
Note: In a practical application, we typically use
HttpClient
within services rather than directly inside our components, then inject those services into our components. This strategy enhances the maintainability and testability of the code.
Fetching Data from an API
Next, we'll fetch data from an API using the HttpClient.get()
method.
@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;
}, (error) => {
console.log('Error:', error);
});
}
}
Displaying a Loading Spinner
While the data is in the process of being fetched, it's important to provide the user with visual feedback. Let's create a simple loading template that's visible when the data is loading and hidden once the data has been fetched.
First, add an isLoading
property to your component:
@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:
<ng-container *ngIf="isLoading">
<!-- Display your loading spinner or skeleton here -->
Loading...
</ng-container>
<ng-container *ngIf="!isLoading && data">
<!-- Display your data here -->
{{ data | json }}
</ng-container>
Error Handling
Lastly, let's add error handling to our data fetch operation. Introduce a new error
property that will be set if an error arises during the fetch operation.
@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);
});
}
}
In your component's template, you can now exhibit an error message to the user if an error occurred:
<ng-container *ngIf="isLoading">
Loading...
</ng-container>
<ng-container *ngIf="error">
{{ error }}
</ng-container>
<ng-container *ngIf="!isLoading && !error && data">
<!-- Display your data here -->
{{ data | json }}
</ng-container>
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.
Simplifying Things: Streamlining Loading State
Our component currently tracks the loading state with multiple properties (isLoading
and error
), and our template has several ngIf
statements. While functional, this adds complexity and potential for errors, plus every ngIf
statement requires additional testing. To streamline our code, we can use Angular techniques.
One such technique combines the async
pipe with ngIf
, automatically subscribing to an Observable and updating our view. It also unsubscribes automatically when the component is destroyed, preventing memory leaks and boosting performance if you enable onPush change detection.
To achieve this, let's modify how we fetch data. We'll move our HTTP request into a new property, data$
, using the pipe
operator to handle errors.
import { of, BehaviorSubject } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Component({
// rest of the component metadata...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent implements OnInit {
data$: Observable<any>;
error$ = new BehaviorSubject<string>(null);
constructor(private http: HttpClient) {}
ngOnInit() {
this.data$ = this.http.get('https://api.mywebsite.com/data').pipe(
catchError(error => {
this.error$.next('An error occurred while fetching data');
console.log('Error:', error);
return of(null);
})
);
}
}
In the template, we can use ngIf
, async
, and else
to manage the loading state, display data, or show the loading spinner:
<ng-container *ngIf="data$ | async as data; else loading">
<!-- Display your data here -->
{{ data | json }}
</ng-container>
<ng-template #loading>
<!-- Display your loading spinner here -->
Loading...
</ng-template>
<ng-container *ngIf="error$ | async as error">
{{ error }}
</ng-container>
Here, we use ng-container
to hold our *ngIf
directive. It's a logical container used to group nodes but is not rendered in the DOM. ng-template
is a template element used with structural directives like *ngIf
.
By adopting this approach, we've eliminated one ngIf
and the isLoading
property from our component, simplifying our code. Furthermore, the async
pipe handles unsubscribing from the observable, averting potential memory leaks.
Striving for Better: Further Simplification and Drawbacks
While we've made significant improvements with the async
pipe and *ngIf
else
technique, there's still room for optimization. Our current solution necessitates manual error catching and an additional ngIf
statement. It also makes an assumption that could potentially cause bugs: it equates falsy data (like false
, ""
, null
, undefined
, 0
) with a loading state.
For instance, if the API response is legitimately falsy, our implementation might fail. Consider an endpoint that might return 0
—a valid response but also falsy in JavaScript. In such cases, our loading spinner would be displayed indefinitely, incorrectly interpreting 0
as an ongoing loading operation.
Moreover, manually catching errors, as we're currently doing, can be tedious and prone to errors. A more declarative approach would enhance code readability and reduce mistakes.
In the next section, we'll explore a solution that addresses these challenges.
Ultimate Solution: *ngxLoadWith
*ngxLoadWith
is the ultimate solution to streamline and optimize the loading state process. *ngxLoadWith
is an Angular structural directive, similar to *ngIf
, that automatically manages all the loading state tracking and template handling for you. This tool not only brings convenience but also boosts performance and maintains a lightweight profile with zero dependencies.
Importantly, *ngxLoadWith
correctly handles falsy values. So, even if your API response is a falsy value like 0
, the loading states will be accurately represented. It also takes care of automatic Observable unsubscription and change detection, making it a robust tool for optimizing application performance. With *ngxLoadWith
, you can enable the OnPush
change detection strategy without a second thought.
With this library, you can keep your components clean and focused, free from loading-related logic. It's a truly declarative and expressive approach to handling loading states in Angular.
To install ngx-load-with, run the following command:
npm install ngx-load-with
To use ngx-load-with, import the NgxLoadWithModule module in your Angular module:
import { NgxLoadWithModule } from "ngx-load-with";
@NgModule({
imports: [NgxLoadWithModule, ...],
declarations: [MyComponent],
...
})
export class MyModule {}
Basic Usage
To load data from an Observable and display it in your template, use:
<ng-container *ngxLoadWith="data$ as data">
<!-- Display your data here -->
{{ data | json }}
</ng-container>
@Component(...)
export class MyComponent {
data$ = this.http.get('https://api.mywebsite.com/data');
constructor(private http: HttpClient) {}
}
Loading and Error Templates
To display a loading message while data is being loaded and an error message if an error occurs, use:
<ng-container *ngxLoadWith="data$ as data; loadingTemplate: loading; errorTemplate: error">
<!-- Display your data here -->
{{ data | json }}
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
<ng-template #error let-error>
{{error.message}}
</ng-template>
See it in action in this Live Example
You can find more information about the *ngxLoadWith
, including detailed documentation and more usage examples, on GitHub.
If *ngxLoadWith
proves useful in your projects, please consider giving it a 🌟 star on GitHub. Your support helps bring attention to the library and contributes to its continued development.
Through this tutorial, you've learned to effectively manage loading states and errors in Angular. As you continue to build and refine your applications, remember these techniques to provide a seamless, user-friendly experience. Happy coding!
Latest comments (0)