Resolving your data quickly with Angular resolvers improves page loading, user experience, and lets you handle errors immediately. We’ll create a Profile Resolver to resolve profile data.
Why use an Angular Resolver?
Asa developer, you are always looking to optimize your code. This includes the speed at which you present the user with the fully loaded UI, which is often dependent on data coming from a database.
Inevitably, you begin to look for ways to resolve data immediately upon navigation to a new page/area of your application, without the user encountering what is known as page JANK.
This is when the page is moving around up and down while loading certain components. It can significantly affect the UX to the point where it looks ‘buggy’.
Angular provides an intuitive approach to pre-fetching data before the route loads; before the navigated route resolves.
It is called an Angular Resolver.
An Angular Resolver is essentially an Angular Service. An injectable class that you provide to a routing module in the route configuration. This special type of service is injected and executed when the containing route is navigated to.
The Resolver then resolves the data before the page load, which becomes available through the ActivatedRoute
service. This provides a simple and efficient way to ensure that your user has the data as quickly as possible before a component that is important to the initial page load would need it.
Another way to use an Angular Resolver is by using it as a method to populate the SEO metadata instantly.
With a resolver, you are providing a guarantee that data will exist before the page has loaded, ensuring that everything has what it needs on initialisation.
Let’s breakdown the Angular Resolver
An Angular resolver is a class that implements the Resolve
interface. The Resolve
interface requires that you implement a function within the class called resolve
.
Here is the Resolve interface signature…
export interface Resolve<T> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T {
return 'data';
}
}
As we can see from the interface signature, it requires a generic argument
T
which will be the type of our resolved data.
The resolve function returns an Observable
, Promise
or just the data of type T
. Therefore, there is a suggestion that this should be handled asynchronously, especially if retrieving the data from the database.
The main purpose of the resolve function is that it has to complete. This fact needs to be remembered when retrieving data in observables. The observable must complete. After all, it is a resolver.
If the observable does not complete, then the data will never resolve, and the page will never load. Therefore, you need to define the point at which you don’t need to take any more values and the data can resolve, as you have got all you need from the database. When using asynchronous data streams such as observables, this is a use case for pipeable operators from RXJS.
The pipeable operators that spring to mind when thinking about completing a data stream based on a condition, is a combination of filter
,take
, first
. With this combination, you can filter all values that you don’t want to take, such as null
or undefined
or an empty array []
, then take
the first valid value with take(1)
.
Operators that may be needed to complete an observable early when encountering issues or errors, in which you’ll want to return null or redirect, are catchError
and timeout
. A combination of timeout
and catchError
is useful if your data is taking too long and you’d like to return null
so that you can try again inside the component, or you’d like to redirect.
If your data doesn’t resolve quickly, is based on complex filtering, logic, huge amounts of database calls, you are likely to experience issues from time to time.
It is best to determine the least amount of database calls, and minimum data that is needed to successfully and gracefully load the page.
Consequently, you may benefit from spending some time, before implementing your resolver, to focus on separating the ‘above the fold’ content from data that can be loaded when the page initializes.
Therefore, you can divide the data necessary for a smooth UX, from the rest of the data that could be called from the component, rather than the resolver.
You can then exclusively deal with the minimal, above the fold, content through the resolver.
This dynamic approach to page loading can be assisted with the use of skeletons. So that if the user scrolls down instantly, you can give the user the indication that the content is loading, subsequently improving UX.
Step 1: Creating the Resolver
We need to create the Angular Resolver. However, there isn’t an Angular CLI command that generates a resolver.Therefore, we will have to write the decorator (resolver metadata) ourselves.
Fortunately, it is only a few lines of code that form the boilerplate for a resolver, and we can take the injectable decorator from an existing service if you’re struggling to remember it.
Annotate the Profile Resolver class with an injectable decorator
First, we provide the injectable decorator with providedIn: any
in the configuration.
@Injectable({ providedIn: 'any'})
We will then name our resolver by appending the convention Resolver
. For this example, we will be resolving profile data (user data), so we will call it ProfileResolver
.
As it is a resolver, and Angular recognizes the function of resolvers, we can implement the Resolve
class, which will provide the signature that we have to implement in our resolve function to successfully resolve the data.
@Injectable({providedIn: 'any'})
export class ProfileResolver implements Resolve<Profile> {
}
Our resolve function will return an Observable with data conforming to theProfile
interface. Therefore, we will provide the Profile
interface as the generic argument to the resolver class and the resolve()
function. This way we have conformed to Angular requirements for a resolver.
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T {
return;
}
The resolve implementation gives us two parameters if they’re needed, route
andstate
. These are automatically populated and are accessible from within our resolve()
function.
Next, we need to actually resolve real data from the database, which will be our next step.
Retrieving the data from the database
To retrieve the data for our resolve function, we will need to inject the service that provides the data; one that interacts with the database.
We take what we need from it to resolve quickly so that the user navigates promptly and successfully. For the purposes of this demo, we won’t worry about the underlying service that deals with the database. We will just inject the service using dependency injection in the constructor argument for ourProfileResolver
class.
As our data comes in the form of an Observable data stream with multiple values emitting asynchronously, we will just need to take(1)
using the pipeable operatortake
imported from rxjs/operator
. Otherwise the observable would never complete, and the resolver would never…resolve.
We just need one emission/value and take
completes the observable for us.
It's as simple as that to create a resolver; we just need to return the observable in the resolve()
function which angular will handle the subscription of.
import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Profile } from '@globals/classes/profile'; import { SeoService } from '@services/seo/seo.service'; import { ProfileService } from '@services/profile/profile.service'; import { from, Observable } from 'rxjs'; import { catchError, take, tap } from 'rxjs/operators'; @Injectable({ providedIn: 'any' }) export class ProfileResolver implements Resolve<Observable<Profile | boolean>> { constructor(private profileService: ProfileService, private router: Router, private seoService: SeoService) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Profile | boolean> { if (!route.paramMap.has('userSlug') { return from(this.router.navigate(['/search'])); } const userSlug = route.paramMap.get('userSlug'); return this.profileService.getProfile$(userSlug) .pipe(take(1), tap(user => { this.seoService.updateTitle(`${ user.displayName }'s Profile`); }), catchError(() => from(this.router.navigate(['/profiles']))); } }
We handle any errors in retrieving the data by redirecting to a parent route.
Bonus: Populate Dynamic SEO metadata quickly before the route has loaded
The benefits of populating our
<meta>
tags instantly has obvious benefits. The quicker our SEO metadata is populated, the faster our SEO correctly and accurately reflects the page content.This means it is easier and quicker for robots, such as those operated by search engines like Google and Bing, to crawl your site and retrieve the content.
This isn’t so important on pre-rendered pages or those that are rendered by Angular Universal, because all of the rendering is complete before the robots receive the content.
However, if you’re relying on the (often questionable) ability for google robots to parse javascript for your SEO, or you’ve got an on-demand solution like puppeteer that needs the assurance that the SEO will be correct before returning the rendered DOM, then a resolver that includes SEO should help. So it helps when the crawler is time-limited.
It also separates concerns from the component, so that the component doesn’t have to deal with anything SEO related. One of the main reasons why I like resolvers.
Step 2: Inject the resolver into the routing module
The ProfileRoutingModule where we will be providing our resolver is a lazy loaded module. Therefore, our root path will be empty with the parameter token
userSlug
, which we will need to retrieve the correct profile data.To provide our resolver, we just provide an object with the name of our data as the key and the specific resolver as the value that will be responsible for resolving that data.
You can name the key anything you like, but we’ll just call it data.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ProfileResolver } from '@app/resolvers/profile-resolver'; const routes: Routes = [ { path : ':userSlug', resolve: { data: ProfileResolver } } ]; @NgModule({ imports: [ RouterModule.forChild(routes) ], exports: [ RouterModule ] }) export class ProfileRoutingModule {}That’s all that is required in the routing module for us to use our resolver.
Next, we need to retrieve and use our data in the component.
Step 3: Initialise the component with resolved data
Now that we have resolved our data on route activation, the data is accessible through the
ActivatedRoute
Service. As we are dealing with observables, throughout the application, we will create a stream that binds to thedata
property which will be our resolved data.First, we’ll inject the
ActivatedRoute
into the constructor of ourProfileComponent
. Next, we’ll assignthis.route.data
to theprofile$
observable. We’ll also want to switch to using an observable when updated data arrives from the database so that we have fresh data when we are interacting with the app.For this, we will use
startWith
so that we start our stream with the value that is readily accessible fromthis.route.snapshot.data
. We then access thedata
property likethis.route.snapshot.data['data']
.startWith
indicates a value to start with, as the first emission of our stream.import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { concatMap, pluck } from 'rxjs/operators'; import { ProfileService } from '@services/profile/profile.service.ts'; @Component({ selector : 'ngx-profile', templateUrl : './profile.component.html', styleUrls : [ './profile.component.scss' ], changeDetection: ChangeDetectionStrategy.OnPush }) export class ProfileCardComponent implements OnInit { profile$: Observable<Profile>; constructor(private route: ActivatedRoute, private profileService: ProfileService) {} ngOnInit(): void { this.profile$ = this.route.params.pipe(concatMap(({userSlug}) => this.profileService.getProfile$(userSlug))) .pipe(startWith(this.route.snapshot.data['data'])); } }What immediately accessible data does for the component
Immediately accessible data reduces time spent loading the individual parts of that page, which is observed by the user. The result of not using a resolver like this is that the page may appear to load in a fragmented way, which is not visually pleasing.
Consequently, you’ll need to pay attention to which elements of your HTML templates are dependent on what data. You should then write your resolver to support these elements and the overall effect on page loading UX.
There are multiple ways that the component can load fragmented
- One of these is if you have an
ngIf
in multiple parts of your HTML template. - Another is
ngFor
.
It is best practice to limit the amount of individual
ngIf
‘s you write for the purposes of limiting the amount of resizing the browser has to do.
Loading the page before getting the data can cause parts of your page to jump, lag and resize constantly, causing the UX to suffer.
Implementing a resolver could be the difference between the user experiencing 3–5 seconds of jumping and resizing vs. 0.5 seconds, which is often too quick to be damaging to the overall UX.
That’s it! We have a resolver with an improved UX on page load.
Any questions, let me know in the
Comments
Post a Comment