When developing Angular applications, it’s common for pages to transition through three key states: error, loading, and show content. Every time you fetch data from an API, your page will likely show a loading indicator first, and then either render the content successfully or display an error message if something goes wrong.
This pattern is repetitive, but there’s a more elegant way to handle it by creating a reusable page component. In this post, I’ll show you how to implement this pattern and how to extract it into a reusable component.
The Pattern
The page lifecycle typically involves three key states:
- Error State: If an error occurs, the error message is shown and this takes precendence over showing anything else.
- Loading State: While data is being fetched, a loading indicator is displayed.
- Content State: Once the data is successfully fetched, the content is displayed.
The logic behind the order of this pattern is that showing errors takes precendence over anything else, then loading and then loading is done we show the content.
Here’s how we can implement this pattern:
import { Component, signal } from '@angular/core'; import { catchError, of } from 'rxjs'; import { ApiService } from './api.service'; @Component({ selector: 'app-data-page', template: ` @if (hasError()) { <p>Error occurred!</p> } @else if (isLoading()) { <p>Loading...</p> } @else { <div>{{ data() }}</div> } `, }) export class DataPageComponent { private data = signal<any>(null); private isLoading = signal(true); private hasError = signal(false); constructor(private apiService: ApiService) { this.fetchData(); } // Called from resolver fetchData() { this.apiService.getData() .pipe( catchError(() => { this.hasError.set(true); return of(null); }) ) .subscribe(data => { this.isLoading.set(false); if (data) this.data.set(data); }); } }
In this example, we use the following order for rendering the page:
- Error state
- Else if loading
- Else content
First, if there are any errors we will show that, then if we see if we are still loading and when we are done loading we show the content.
The Reusable Page Component
We can extract this pattern into a reusable page component to avoid duplicating it on every page. Here’s how we can implement it for error, loading and content states:
import { Component, effect, inject, input, TemplateRef, ContentChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { LoadingComponent } from '../loading/loading.component'; import { ErrorComponent } from '../error/error.component'; import { LayoutService } from '../layout/layout.service'; @Component({ selector: 'lib-page', standalone: true, imports: [CommonModule, ErrorComponent, LoadingComponent], template: ` @if (error()) { <ng-container *ngTemplateOutlet="errorTemplate || defaultErrorTemplate"></ng-container> } @else if (isLoading()) { <ng-container *ngTemplateOutlet="loadingTemplate || defaultLoadingTemplate"></ng-container> } @else { <ng-content></ng-content> } <!-- Default templates --> <ng-template #defaultErrorTemplate> <lib-error></lib-error> </ng-template> <ng-template #defaultLoadingTemplate> <lib-loading></lib-loading> </ng-template> `, styles: ` :host { display: block; } `, }) export class PageComponent { @ContentChild('loadingTemplate', { static: false }) loadingTemplate?: TemplateRef<any>; @ContentChild('errorTemplate', { static: false }) errorTemplate?: TemplateRef<any>; error = input<Error | null>(null); isLoading = input<boolean>(false); title = input<string>(''); private layoutService = inject(LayoutService); constructor() { effect( () => { this.layoutService.setPageTitle(this.title()); }, { allowSignalWrites: true } ); } }
Explanation:
- First we check the page states: first for the error, then for loading, and finally showing the content.
- The component takes in contentChildren for overriding the default
LoadingComponent
andErrorComponent
. - The
title
input updates the page title dynamically using a service. - This component is standalone and can be reused across your application to handle the common page states and logic.
Using the Reusable Page Component
Now, you can use this reusable component in your pages with custom or default content as follows:
<lib-page [error]="hasError()" [isLoading]="isLoading()" [title]="'My Page Title'"> <div>{{ data }}</div> </lib-page>
In this example:
- If an error occurs,
<lib-error>
is displayed. - If data is loading,
<lib-loading>
is shown. - Otherwise, the
ng-content
passed to<lib-page>
tags is rendered.
GitHub Repo
The template template in action looks like this:
You can find the full implementation and example usage in the GitHub repo. Make sure to check it out for more details and code samples.
Conclusion
Handling error, loading and content states is a common pattern in Angular applications. By using the @if
syntax and creating a reusable page component, you can write more maintainable and concise code. This approach reduces repetition and provides consistency across your app.
Feel free to explore the GitHub repo for more examples, and I hope you find it useful!
Do you want to become an Angular architect? Check out Angular Architect Accelerator.