Skip to main content

Loading data before components in Angular

4 min read

Older Article

This article was published 8 years ago. Some information may be outdated or no longer applicable.

We’ll walk through a few ways to make sure data is ready for a component before it loads and appears on screen.

Setup the app

First, let’s scaffold an application using the angular-cli. Notice the routing flag, which gives us a routing module out of the box: ng new route-change-app --routing.

Let’s also generate two new components: ng g c home && ng g c products.

We also need to update app-routing.module.ts and add our routes:

// excerpt
import { HomeComponent } from './home/home.component';
import { ProductComponent } from './about/product.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'products', component: ProductComponent },
];

And finally, open app.component.html and add this HTML:

<nav>
  <a routerLink="/">Home</a> |
  <a routerLink="/products">products</a>
</nav>
<router-outlet></router-outlet>

A slow API

In most full-stack JavaScript applications, we’re going to talk to an API. That means our app’s speed is partly tied to how fast the API responds.

For demonstration purposes, let’s fake a slow API. Create a service via the CLI called ApiService: ng g s api.

To keep things simple, we won’t use an actual backend. Instead, we’ll return static data via an Observable with a delay.

The API service should look like this:

import { Injectable } from '@angular/core';
import { of, Observable } from 'rxjs';
import { delay } from 'rxjs/internal/operators';

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  constructor() {}

  getProducts(): Observable<any> {
    const products = [
      { name: 'shoe', price: 15.99 },
      { name: 'shirt', price: 25.99 },
      { name: 'jeans', price: 54.5 },
    ];
    return of(products).pipe(delay(3000));
  }
}

Now let’s consume this API from the ProductComponent:

import { ApiService } from '../api.service';
// ... more code
constructor(private api: ApiService) { }
products;
ngOnInit() {
  this.api.getProducts().subscribe(products => this.products = products);
}

And display it in the component template:

<ul>
  <li *ngFor="let product of products">
    {{ product.name }} costs {{ product.price }}
  </li>
</ul>

Standard setup. Launch the application and navigate to the “products” route. You’ll see a blank page for 3 seconds. Then the data arrives and renders.

That’s a problem. For 3 seconds the user has no idea what’s happening. They might assume the site’s broken and leave. Not great.

Let’s look at what we can do about it.

Option 1: ngIf

The most obvious approach: add an ngIf to the template that shows a loading indicator until this.products gets populated:

<p *ngIf="!products">Loading data ...</p>
<ul>
  <li *ngFor="let product of products">
    {{ product.name }} costs {{ product.price }}
  </li>
</ul>

When accessing properties of an object that hasn’t loaded yet, use a Safe Navigation Operator.

Option 2: Router resolver

A router resolver attaches a resolve function to the route that loads a component with an API call. Angular won’t load or display the component until the API call (or whatever else we define) completes.

Let’s see how to set it up.

First, create a separate class for the resolver using the Angular CLI: ng g class resolver.

The class should look like this:

// resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';

@Injectable()
export class Resolver implements Resolve<Observable<string>> {
  constructor(private api: ApiService) {}

  resolve() {
    return this.api.getProducts();
  }
}

Don’t forget to add this class as a provider:

// app.module.ts
import { Resolver } from './resolver';
// ...
providers: [Resolver],

The code above implements the Resolve interface, which requires a resolve() method. Inside that method, we consume the API we built earlier.

Next, update app-routing.module.ts to tell the route where and how to resolve the data:

// app-routing.module.ts
import { Resolver } from './resolver';

const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'products',
    component: ProductsComponent,
    resolve: { products: Resolver },
  },
];

One more change. Right now we’re making an API call in both the product component and the resolver class. That’s a duplicate call we don’t need. So let’s update the product component.

The resolved data is available on the router snapshot, which we access from @angular/router:

// products.component.ts
import { ActivatedRoute } from '@angular/router';
// ...
constructor(private route: ActivatedRoute) { }
products;
ngOnInit() {
  this.products = this.route.snapshot.data.products;
}

Now refresh the application and click the Products link. You’ll notice a delay before the route changes. Once the API resolves, the route switches and the product list appears.

Conclusion

We’ve looked at a couple of ways to handle situations where a slow API call would leave users staring at a blank screen. Pick one and use it. Your users will stick around longer.