Error, loading, content…? Use this page pattern for your Angular apps

Share on facebook
Share on google
Share on twitter
Share on linkedin

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:

  1. Error State: If an error occurs, the error message is shown and this takes precendence over showing anything else.
  2. Loading State: While data is being fetched, a loading indicator is displayed.
  3. 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:

  1. Error state
  2. Else if loading
  3. 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 and ErrorComponent.
  • 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.

Related Posts and Comments

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »

How to Set Up Git Hooks in an Nx Repo

Git hooks can be used to automate tasks in your development workflow. The earlier a bug is discovered, the cheaper it is to fix (and the less impact it has). Therefore it can be helpful to run tasks such as linting, formatting, and tests when you are e.g. committing and pushing your code, so any

Read More »