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.
Table of contents
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.
Where’s the unit testing for the resolver?