Angular - Resolving Route Data
Bài đăng này đã không được cập nhật trong 3 năm
1. Intro
Today, I will introduce about a powerful technique to achieve the best user-experience when browsing between pages in your Angular application: Resolve. Before digging into this article, I suggest you acquire the knowledge of working with with Angular 2 Router.
2. Understanding the problem
Let’s just stick with the scenario of a user management application. We have a route for a user list and a route for user detail. Here is what the route configuration looks like:
//app.router.ts
import { Routes } from '@angular/router';
import { USerListComponent } from './user-list';
import { UserDetailComponent } from './user-detail';
export const AppRoutes: Routes = [
{ path: '', component: UserListComponent },
{ path: 'user/:id', component: UserDetailComponent }
];
And of course, we use that configuration to configure the router for our application:
//app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppRoutes } from './app.routes';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(AppRoutes)
],
...
})
export class AppModule {}
Nothing special going on here. In case that all of these above seem new to you, please take a look at Angular Router.
Let’s take a look at the UserDetailComponent
. This component is responsible for displaying user data, therefore, it somehow has to get access to a User
object, that matches the id
provided in the route URL (hence the :id
parameter in the route configuration).
We can easily access route parameters using the ActivatedRoute like this:
//user-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UserService } from '../user.service';
import { User } from '../interfaces/user';
@Component({
selector: 'user-detail',
template: '...'
})
export class UserDetailComponent implements OnInit {
user: User;
constructor(
private userService: UserService,
private route: ActivatedRoute
) {}
ngOnInit() {
let id = this.route.snapshot.params['id'];
this.userService
.getUser(id)
.subscribe(user => this.user = user);
}
}
Okay, cool. So the only thing UserDetailComponent does, is to fetch an User
object by the given id and assign that object to its local user
property, which then allows us to interpolate expressions like {{user.name}}
so that it can be displayed in the template of the component.
Let’s take a look at the component’s template:
<!-- user-detail.html -->
<h2>{{user?.name}}, {{user?.age}}, {{user?.occupation}}</h2>
Everything so far so good. But let's try to put this scenario into reality: in our project, we never display data in such a simple and trivia format like the template above. Instead, our template of user detail might have the "glamorous" appearance like this:
<!-- user-detail.html -->
<h2>{{user?.name}}
<span>Age: {{user?.age}}</span>
<span>Occupation: {{user?.occupation}}</span>
Notice that we have attached Angular’s Safe Navigation Operator ?.
to all of our expressions that rely on user
. The reason for that is, that value user
is undefined
at the time this component is initialized, since we’re fetching the data asynchronously. The Safe Navigation Operator ensures that Angular will not throw any errors when we are trying to read from an object that is null or undefined.
In order to demonstrate this issue, let’s assume UserService.getUser()
takes 4 seconds until it emits an user object. In fact, we can easily fake that delay right away like this:
//user.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
getUser(id) {
return Observable.of({
id: id,
name: '緋色死神',
age: 22,
occupation: 'フロンエンド開発者',
}).delay(3000);
}
}
See that now? Notice how the User Interface flickers until the data arrives. At first, these were only the labels there and after 3 seconds, the data is returned successfully and displayed in the template. In some circumstances, who know if these kind of procedures might take up to more 4, 5 seconds?
We have ended up with an UI that already renders its view and a only a few moments later the actual data arrives. What if we do not wish for such behavior?
What if we want the data to be completely displayed as soon as the page is loaded?
It is impossible for now as the UserService().getUser()
is called in the ngOnInit() function, which means only after the component is initialized can the data be fetched.
What if, we manage somehow to fetch the data even before the component initialization?
3. Defining resolvers
As mentioned ealier, route resolvers allow us to provide the needed data for a route, before the route is activated.
There are different ways to create a resolver and we’ll start with the easiest: a function. A resolver is a function that returns either Observable<any>
, Promise<any>
or just data. This is great, because our UserService.getUser()
method returns an Observable<User>
.
Resolvers need to be registered via providers. Our article on Dependency Injection in Angular explains nicely how to make functions available via DI.
Here’s a resolver function that resolves with a static User
object:
//app.module.ts
@NgModule({
...
providers: [
UserService,
{
provide: 'user',
useValue: () => {
return {
id: 1,
name: '緋色死神',
occupation: 'フロンエンド開発者'
};
}
]
})
export class AppModule {}
Let’s ignore for a second that we don’t always want to return the same user object when this resolver is used. The point here is that we can register a simple resolver function using Angular’s dependency injection. Now, how do we attach this resolver to a route configuration? That’s pretty straight forward. All we have to do is add a resolve property to a route configuration, which is an object where each key points to a resolver.
Here’s how we add our resolver function to our route configuration:
//app.routes.ts
export const AppRoutes: Routes = [
...
{
path: 'user/:id',
component: UserDetailComponent,
resolve: {
user: 'user'
}
}
];
That’s it? Yes! user
is the provider token we refer to when attaching resolvers to route configurations.
Now, the next thing we need to do is to change the way UserDetailComponent gets hold of the User object. Remember that everything that is resolved via route resolvers is exposed on an ActivatedRoute’s data property. In other words, for now we can get rid of the UserService dependency like this:
//user-detail.component.ts
@Component()
export class UserDetailComponent implements OnInit {
user;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.user = this.route.snapshot.data['user'];
}
}
Still, for now, a static object (an user {id: 1, name: '緋色死神', age: 22, occupation: 'デザイナー'}
) is returned everytime this route is activated. What we actually need is a corresponding user returned for each unique id and in order to achieve that, we need a UserService instance to save the day, which we don’t get injected here.
So how do we create resolver that needs dependency injection?
4. Resolvers with dependencies
As we know, dependency injection works on class constructors, so what we need is a class. We can create resolvers as classes as well! The only thing we need to do, is to implement the Resolve interface, which ensures that our resolver class has a resolve()`` method. This
resolve()`` method is pretty much the same function we have currently registered via DI.
Here’s what our user resolver could look like as a class implementation:
//user.resolve.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';
@Injectable()
export class UserResolve implements Resolve<User> {
constructor(private userService: UserService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.userService.getUser(route.params['id']);
}
}
As soon as our resolver is a class, our provider configuration becomes simpler as well, because the class can be used as provider token!
//app.module.ts
@NgModule({
...
providers: [
UserService,
UserResolve
]
})
export class AppModule {}
And of course, we use the same token to configure the resolver on our routes:
//app.routes.ts
export const AppRoutes: Routes = [
...
{
path: 'user/:id',
component: UserDetailComponent,
resolve: {
user: UserResolve
}
}
];
Angular is smart enough to detect if a resolver is a function or a class and if it’s a class, it’ll call resolve()
on it.
From now on, Angular will delays the UserDetailComponent
instantiation until the data has arrived.
5. Pitfall
Ok, so Resolve is a powerful technique. So that mean I should implement resolve technique on every route in the configuration to ensure the data is grabbed before component initialization? Should I use resolve to get the data before the UserListComponent
's initialization? I mean, why not? It is merely similar to what I did with the UserDetailComponent
, isn't it?
The answer is not always. You might have ran into this situation many times (probably earlier today): you press a button which triggers a route change. If that route has a resolve that takes a few seconds to complete, you won’t get any feedback about anything happening until the resolves are resolved. This can be frustrating, making you wonder did I click it? Perhaps you’ll click again just to make sure.
You can also, of course, have the button show some spinner or something until the route transition happens. But the more spining wheel, the more it generates feeling of a sloppy and slow application. So the advice here is to consider by yourself and load only important data in resolves to render main parts of the page immediately and fetch all other data, asynchronously.
6. Acronym & Abbreviation
- UI: User Interface
- DI: Dependecy Injection
7. References
All rights reserved