Tutorial

How To Use Route Resolvers with Angular Router

Angular

Introduction

One way for handling retrieving and displaying data from an API is to route a user to a component, and then in that component’s ngOnInit hook call a method in a service to get the necessary data. While getting the data, perhaps the component can show a loading indicator.

There is another way to use what is known as a route resolver, which allows you to get data before navigating to the new route.

One API that is available for use is the Hacker News API. Hacker News is a website for sharing links and discussing them. The API can be used to retrieve the most popular posts and display information about individual posts.

In this tutorial, you will implement a route resolver that gets data from the Hacker News API before navigating to a route that displays the gathered data.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v15.3.0, npm v6.14.9, @angular/core v11.0.1, @angular/common v11.0.1, @angular/router v11.0.1, and rxjs v6.6.0.

Step 1 — Setting Up the Project

For the purpose of this tutorial, you will build from a default Angular project generated with @angular/cli.

  • npx @angular/cli new angular-route-resolvers-example --style=css --routing --skip-tests

This will configure a new Angular project with styles set to “CSS” (as opposed to “Sass”, Less", or “Stylus”), routing enabled, and skipping tests.

Navigate to the newly created project directory:

  • cd angular-route-resolvers-example

At this point, you have a new Angular project with @angular/router.

Step 2 — Building a Resolver

Let’s start by implementing a resolver that returns a string after a delay of 2 seconds. This small proof of concept can help with exploring the fundamentals of wiring routes that can be applied to larger projects.

First, create a separate class for the resolver in a file of its own:

  • ./node_modules/@angular/cli/bin/ng generate resolver news

This will use the @angular/cli to generate a resolver named news:

src/app/news.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';

import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class NewsResolver implements Resolve<Observable<string>> {
  resolve(): Observable<string> {
    return of('Route!').pipe(delay(2000));
  }
}

Implementing the Angular router’s Resolve interface requires the class to have a resolve method. Whatever is returned from that method will be the resolved data.

This code will return an observable that wraps a string after a delay of 2 seconds.

Step 3 — Configuring Routes

In order to experience two different routes, you will need two new components. home will be the landing page. And top will feature the top posts from Hacker News API.

First, use @angular/cli to generate a home component:

  • ./node_modules/@angular/cli/bin/ng generate component home

Then, use @angular/cli to generate a top component:

  • ./node_modules/@angular/cli/bin/ng generate component top

Now you can set up the routing module to include the resolver.

src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { NewsResolver } from './news.resolver';

import { TopComponent } from './top/top.component';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: HomeComponent
  },
  {
    path: 'top',
    component: TopComponent,
    resolve: { message: NewsResolver }
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Notice how the resolver is provided just like a service and then you include the resolver with the route definition. Here the resolved data will be available under the message key.

Step 4 — Accessing the Resolved Data in the Component

In the component, you can access the resolved data using the data property of ActivatedRoute’s snapshot object:

src/app/top/top.component.ts
import { Component, OnInit } from '@angular/core';

import { ActivatedRoute } from '@angular/router';

@Component({ ... })
export class TopComponent implements OnInit {
  data: any;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.data = this.route.snapshot.data;
  }
}

Now, in the component, you can access the Route! message like so:

src/app/top/top.module.html
<p>The message: {{ data.message }}</p>

At this point, you can compile your application:

  • npm start

And visit localhost:4200/top in a web browser.

Output
The message: Route!

You will observe when navigating to the top route that there is now a 2-second delay because the data is resolved first.

Step 5 — Resolving Data from an API

Let’s make things more real-life by actually getting some data from an API. Here you will create a service that gets data from the Hacker News API.

You will need HttpClient to request the endpoint.

First, add the HttpClientModule to app.module.ts:

src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Then, create a new service:

  • ./node_modules/@angular/cli/bin/ng generate service news

This will use the @angular/cli to generate a service named news:

src/app/news.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class NewsService {
  constructor(private http: HttpClient) { }

  getTopPosts() {
    const endpoint = 'https://hacker-news.firebaseio.com/v0/topstories.json';

    return this.http.get(endpoint);
  }
}

And now you can replace the string code in NewsResolver with NewsService:

src/app/news.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs';

import { NewsService } from './news.service';

export class NewsResolver implements Resolve<any> {
  constructor(private newsService: NewsService) {}

  resolve(): Observable<any> {
    return this.newsService.getTopPosts();
  }
}

At this point, if you look at the top route in a browser, you will be presented with a list of numbers representing ids of the top posts on Hacker News.

Step 6 — Accessing Route Parameters

You can get access to the current route parameters in your resolver using the ActivatedRouteSnapshot object.

Here’s an example where you would use a resolver to get access to the id param of the current route.

First, use the @angular/cli to generate a resolver named post:

  • ./node_modules/@angular/cli/bin/ng generate resolver news

Then, modify post.resolver.ts to use ActivatedRouteSnapshot:

src/app/post.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

import { NewsService } from './news.service';

@Injectable({
  providedIn: 'root'
})
export class PostResolver implements Resolve<any> {
  constructor(private newsService: NewsService) {}

  resolve(route: ActivatedRouteSnapshot): Observable<any> {
    return this.newsService.getPost(route.paramMap.get('id'));
  }
}

Next, add a getPost method to NewsService:

src/app/news.service.ts
// ...

export class NewsService {
  constructor(private http: HttpClient) { }

  // ...

  getPost(postId: string) {
    const endpoint = 'https://hacker-news.firebaseio.com/v0/item';

    return this.http.get(`${endpoint}/${postId}.json`);
  }
}

And add PostResolver and the post/:id route to app-routing.module.ts:

src/app/app-routing.module.ts
// ...

import { PostResolver } from './post.resolver';

// ...

const routes: Routes = [
  // ...
  {
    path: 'post/:id',
    component: PostComponent,
    resolve: { newsData: PostResolver }
  }
];

// ...

Next, create the new PostComponent:

  • ./node_modules/@angular/cli/bin/ng generate component post

Then, modify post.component.ts to use snapshot data:

src/app/post/post.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({ ... })
export class PostComponent implements OnInit {
  data: any;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.data = this.route.snapshot.data;
  }
}

And modify post.component.html to display the title:

src/app/post/post.component.html
<p>{{ data.newsData.title }}</p>

Now if a user goes to http://localhost:4200/post/15392112, the data for the post id 15392112 will be resolved.

Step 7 — Handling Errors

In case there’s an error while fetching the data, you could catch and deal with the error in the resolver using RxJS’s catch operator. Something like this for example:

src/app/news.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';

import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { NewsService } from './news.service';

@Injectable()
export class NewsResolver implements Resolve<any> {
  constructor(private newsService: NewsService) {}

  resolve(): Observable<any> {
    return this.newsService.getTopPosts().pipe(catchError(() => {
      return of('data not available at this time');
    }));
  }
}

Or you could return an EMPTY observable and return the user to the root path:

src/app/news.resolver.ts
import { Injectable } from '@angular/core';
import { Router, Resolve } from '@angular/router';

import { Observable, EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { NewsService } from './news.service';

@Injectable()
export class NewsResolver implements Resolve<any> {
  constructor(private router: Router, private newsService: NewsService) {}

  resolve(): Observable<any> {
    return this.newsService.getTopPosts().pipe(catchError(() => {
      this.router.navigate(['/']);
      return EMPTY;
    }));
  }
}

These two approaches will lead to a better user experience if there is an error when retrieving data from the API.

Conclusion

In this tutorial, you implemented a route resolver that gets data from the Hacker News API before navigating to a route that displayed the gathered data. This was achieved by utilizing @angular/router, @angular/common/http, and rxjs.

If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.

Creative Commons License