08 January 2017 Angular 2 Route Resolves in 10 Minutes

The goal of this post is to demonstrate how Angular 2 route resolves work in 10 minutes, along with providing sample code in Plunker. If you are already familiar with route resolves from Angular 1, I recommend skipping the intro and jumping straight to the sample app section.


Table of Contents

Intro
Sample App

Notes & Tips

Closing Notes


Intro

Route resolves are nothing more than a way to pre-fetch the data a component needs before it is initialized. Usually this data comes from an API. Let’s say you have a component whose only role is to display a chart of daily sales for the month; there is no point in rendering the view or loading this component before the sales data is available. In fact, many charting libraries will throw an error if you try to initialize a chart before supplying it with data (and so will *ngFor). Of course, you can easily work around this problem by hiding the html with an *ngIf or temporarily supplying an empty array until the necessary data is loaded. However, while you can get by without resolves, implementing them helps make code more readable and maintenable by:

  1. Eliminating confusing clutter in your component’s template and code.
  2. Clarifying your intent by showing which data must be pre-fetched.

Despite these benefits, many sites avoid using resolves in favor of displaying most of the component and showing a spinner in the sections for which data is still being loaded. In some cases, this approach is desirable from a UX point of view, and a resolve should not be used. Use your discretion.


Sample App

Open Sample App

As you can see, our starting point is a very basic app with two routes - “Home” and “News”. You navigate between the routes by clicking on the corresponding tab. We are going to add a resolve to pre-fetch news from an API before the News component loads. This will take three steps:


Step 1: Create a resolver class which makes an Http call to pre-fetch the data we need.

Create a new Typescript file and name it:

src/api-news-resolver.service.ts

Then copy and paste the following code into your new file (explained below):

import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class ApiNewsResolver implements Resolve<any> {

  constructor(private http: Http) { }

  resolve(): Observable<any> {
    let newsUrl = 'http://httpbin.org/post';
    let news = 'The sky is blue'; //Mock data to be returned by test API
    
    return this.http.post(newsUrl, news)
      .map( (res) => res.json() )
      .catch( (err) => Observable.throw(error.json().error) );
  }
}

Let’s break down what we did in the code above:

  • Added ES6 import statements to bring in the necessary modules.

  • Created a new TypeScript ApiNewsResolver class.

  • Added the Resolve interface to our class - this is OPTIONAL, but any class we plan to use as a resolver must implement a resolve method, so it’s a good convention.

  • Added a resolve() method to ApiNewsResolver - this is the method responsible for returning the data we need. Naming the method that returns data for a resolve guard `resolve’ is an Angular 2 convention, and is not optional - the resolver would not work if it was named anything else.

  • If you noticed that a POST request is incorrectly being used instead of a GET in the code above, you are absolutely right; In a real app, this would be a GET request. The examples here take advantage of http://httpbin.org/, which is a site providing test API endpoints.

Before moving on, we must include the resolver class we just created in our module. Navigate to src/app.ts and add ApiNewsResolver to the providers array. If you are just starting to work with Angular 2, simply replace the contents of src/app.ts with the code below - changes are marked with notes:

import { Component, NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';

import { ApiNewsResolver } from './api-news-resolver.service' //Import our new resolver 
import { HomeComponent } from './home.component'
import { MainComponent  from './main.component'
import { NewsComponent } from './news.component'

@NgModule({
  imports: [ 
    BrowserModule, HttpModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent },
      { path: 'home', component: HomeComponent },
      { path: 'news', component: NewsComponent }
    ])
  ],
  declarations: [ MainComponent, HomeComponent, NewsComponent ],
  providers: [ ApiNewsResolver ], //Add ApiNewsResolver to the providers array
  bootstrap: [ MainComponent ]
})

export class AppModule {}

With this done, we have now defined the resolver class. In the following steps, we will add it to our route.

Code After Adding the Resolver Class


Step 2: Add a Resolve Guard to the route.

In src/app.ts, change the following line of code { path: 'news', component: NewsComponent } to:

{ path: 'news', 
  component: NewsComponent, 
  resolve: {
    news: ApiNewsResolver
  }
}

All we did here was add the resolve guard to our news route. This tells Angular that we must wait for ApiNewsResolver’s resolve() method to return data before we display NewsComponent.

It’s important to point out that news on this line news: ApiNewsResolver of code is what I chose to name whatever data is returned by the resolver. You can name it anything you want.

As a bit of a tangential note - If you are unfamiliar with Angular 2 route guards in general and would like to know more, the documentation is here. Going into details about guards is outside of the scope of this post, but you should know that there are other guards besides resolve available, which is why I brought up the term.

Code With The Resolve Guard Added


Step 3: Get the resolved data from the component’s activated route.

Now that we have a resolver class and we added it to our route, data is being pre-fetched. Now we just need to access it in the component. In our case, the component is NewsComponent located in src/news.component.ts. Navigate to that file and add the following ES6 module:

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

then provide ActivatedRoute in the News component’s constructor by adding the following code at the top of the NewsComponent class definition:

constructor(private route: ActivatedRoute){}

As you know, or might have guessed, ActivatedRoute allows us to access information about the route which is currently active, such as the url of the route, query parameters, etc. What we care about here is the news data which gets loaded into the ActivatedRoute from the resolve guard. To get the resolved news data from the route, add a property to hold the news data:

public news: any;

and then get the data from the activated route when the NewsComponent is initialized:

  ngOnInit(): void {
    this.news = this.route.snapshot.data['news'];
  }

That’s virtually it. Change the news component template to display the news from the route resolve by removing the current contents:

<div>This is just a placeholder for now. News will go here. </div>

and replacing them with:

{{news.data}}

You should now be greeted with the latest news, “The sky is blue”, when you click the News tab.

Finished Code


Notes & Tips

• Resolves can cause redundant API calls: A resolve gets data every time a component is loaded. This often results in unnecessary API calls which adversely affect performance. If your resolve gets data that doesn’t change frequently, consider writing the data returned by the resolve to a property in the resolver class and simply returning that property if it has already been set. For instance, in our example, we would add an initially undefined news property like so: public news: any = undefined; in our news resolver. Then, in the resolve() method, check if the news property is already set and return its value without making an API call if it is, i.e.:

  resolve(): Observable<any> {
    if(this.news) {
      return this.getSavedNews();
    } else {
      return this.getNewsFromApi()
    }	
  }

Return Saved Data, if Already Fetched from API

Naturally, you could go further and set a time period during which the data is valid by not only saving the data, but adding another timestamp property and making an API call if the the data is older than x.


• Like in Angular 1, you can use multiple resolves on the same route. The resolve calls are made in parallel and the component will load only after all of the calls return data. To use multiple resolves, simply add them to the route:

  { path: 'news', 
	component: NewsComponent, 
	resolve: {
	  news: ApiNewsService, //first resolve
	  otherNews: ApiOtherNewsService // second resolve
	}
  }

Then additional resolve data can be accessed from the route snapshot just like a single resolve:

  ngOnInit(): void {
    this.news = this.route.snapshot.data['news'];
    this.otherNews = this.route.snapshot.data['otherNews'];
  }

Code With Multiple Resolves


• The resolver has access to route params. Let’s say you have a component that displays a list of news story titles. When a story is clicked, another component which shows the actual news article is opened. Before loading that component, we need to pre-fetch the content of the news story whose title was clicked - this can be done in the resolve method. The resolver class has access to the ActivatedRoute, so we can get the id of story that was clicked:

  resolve(route: ActivatedRouteSnapshot) {
    let id: any = route.params['id']);
    return this.getNewsStory(id);
  }

This is fairly self-explanatory. For an example, check out the new src/news-story-resolver.service.ts file in the link below. The links to the new component were added in the News tab (src/news.component.ts).

Code With Parameterized Resolve


Closing Notes

This was just a brief overview of how to use route resolves. The documentation is getting better every day, but I always find it helpful to have working code examples - hopefully these help you as well.

The code I provided is generally similar to what you would use in production, with a notable exception; The Angular team recommends moving routing out into its own module. I intentionally didn’t do this here for the sake of simplicity and clarity. You can find details on moving your app’s routing out into a separate Routing Module in the official Angular 2 app example. This section is well written and detailed:

Angular 2 - Move Routing Out Into a Separate Module

Victor C · Software Developer