I often come across Angular components that retrieve data from a remote API inside the ngOnInit lifecycle hook. While this is an easy way to load data into the component, it can can increase the size of the component and lead to duplication.

A better approach is to use route resolvers to provide data to Angular components before the component loads.

Benefits of using resolvers in Angular

A route resolver is just a class, which implements the Resolve interface. Through this interface, a class can become a data provider to return data to the route.

Benefits of using a route resolver in Angular:

  • Simplify component initialisation code by moving retrieval code to resolvers
  • Increased maintainability due to compliance with single responsibility principle. The component is responsible for display, not for data retrieval
  • Data is available before the component loads
  • There is no need to inject services into the component as the data is accessible from the active route
  • Multiple components on the same route can access retrieved data
interface Resolve<T> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) 
: Observable<T>
}Code language: TypeScript (typescript)

The interface is really simple. The only requirement is to return an Observable<T>.

T here is the instance type you wish to make available to the route. It can be anything from a string, number or even an object.

The two parameters are optional but you most likely need the ActivatedRouteSnapshot to access the parameters in the route. It is a good idea to add it to your implementation.

Resolver example

The following class is a simple route resolver that makes a customer available to its associated route.

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
@Injectable({providedIn : "root"})
export class CustomerResolver implements Resolve<Observable<string>> {
  
  constructor(private customerService: CustomerService) {}
   resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<string> {
    const id = route.params["customerId"];
    return this.customerService.get(id);
  }
}Code language: TypeScript (typescript)

Our simple CustomerResolver implements the Resolve interface with type string.

Firstly, it reads the customer id from the route object. Then it returns an Observable<string> through the CustomerService using the customer’s id.

Incase you’re wondering what the get method looks like, here is the signature:

// Given an id, returns the customer name
public get(id: string) : Observable<string> { 
   // return customer from the API 
}Code language: TypeScript (typescript)

Configure routing

We are now ready to make use of the CustomerResolver. We just need to tell Angular which routes to use the resolver for.

import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { CustomerResolver } from "../core/resolvers/customer.resolver";
import { CustomerComponent } from "./customers/customer.component";
const routes: Routes = [
    {
        path: "customer",
        component: CustomerComponent,
        resolve: { customer: CustomerResolver }
    }
];
@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class CustomersRoutingModule { }Code language: TypeScript (typescript)

The routing configuration above is pretty standard apart from the resolve setup.

We provide a JSON object with a customer key and assign the resolver to it.

This tells Angular that the CustomerResolver is to be executed and its result should be stored in the router with the key customer.

There is no need to provide the resolver as it is marked Injectable.

Getting resolved data in components

Now that we the resolver is setup, we can access the customer data inside the component. We will inject the ActivatedRoute service into the component to access the customer.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ ... })
export class CustomerComponent implements OnInit {
  private customer: string;
  constructor(private route: ActivatedRoute) {}
  ngOnInit() {
    this.customer = this.activatedRoute.snapshot.data["customer"]);
  }
}Code language: TypeScript (typescript)

That’s it! We have just completed implementing a resolver in Angular.

We can now access the customer name in the component template like so:

{{ customer }}Code language: HTML, XML (xml)

You will notice that the resolver will execute before the component loads into the view.

Error handling in route resolvers

In case there’s an error while retrieving the data, you could catch and deal with the error in the resolver using RxJS’s catch operator.

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    const id = route.params["customerId"];
    return this.customerService
               .get(id)
               .pipe(catchError(error => {
                  console.log(error);
                  return of('Failed to get customer');
               }));
}Code language: TypeScript (typescript)

Summary

In this post, we have implemented a simple route resolver. As mentioned, route resolvers allow us to reduce the amount of retrieval code in components, making our code-base more maintainable.

Umut Esen

Software Engineer specialising in full-stack web application development.

Leave a Reply

This Post Has One Comment

  1. John

    Where’s the unit testing for the resolver?