The Angular Party
Let's put the resolver into practice by building an application that displays a list of beers from the https://api.punkapi.com/v2/beers API, using a service.
The application has two routes, ComponentRoom and ResolverRoom, each offering a distinct user experience.
- The Component Room leverages the async pipe to fetch data from the service.
- The Resolver Room employs a resolver to retrieve data, with the component gaining access through
route.snapshot.data.
What needs to be done?
We'll proceed through this step by step.
1- Create an interface to map the API response.
2- Build the beer service to fetch data and provide a subscription with the result.
3- Generate three components: BeerRoom, ResolverRoom, and HomeComponent.
4- Construct the resolver.
5- Register the resolver and define the application routes.
We'll also bring other players into the mix, like Router, ActivateRoute, Observable, and more. Let's get to work!
The source code is available in this repository.
The beer service
First, we set up a Beer interface and a BeerService to supply the data coming from the API.
This Beer interface captures specific attributes from the beer API response.
export interface Beer {
id: number;
name: string;
tagline: string;
first_brewed: string;
description: string;
image_url: string;
}
The BeerService needs the httpClient injected to perform requests against the API. Additionally, it leverages Rxjs to return an observable array of Beer.
We import both httpClient and the Injectable decorator, and develop a getBeers method that returns the result of the request to https://api.punkapi.com/v2/beers. We also apply the delay operator, which slows the response down by 5 seconds.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';
import { Beer } from './models/beer';
@Injectable()
export class BeerService {
public beers$: Observable<Beer[]>;
constructor(private http: HttpClient) {
this.getBeers();
}
private getBeers(): void {
this.beers$ = this.http
.get<Beer[]>('https://api.punkapi.com/v2/beers')
.pipe(delay(4000));
}
}
Learn more about operators and services.
Delay Operator https://www.learnrxjs.io/learn-rxjs/operators/utility/delay
Services https://angular.io/tutorial/toh-pt4
The home component
This serves as the landing page, featuring two links that navigate to the routes named beer-room and resolver-room, implemented with the routerLink directive.
<p class="text-center">
Do you want to join to party and wait for the beers, or when you get in, the
beers are ready ?
</p>
<div class="btn-group btn-group-block">
<a [routerLink]="['/beer-room']" class="btn btn-primary">Component Room</a>
<a [routerLink]="['/resolver-room']" class="btn btn-secondary"
>Resolver Room</a
>
</div>
More about router link https://angular.io/api/router/RouterLink
The BeerRoom Component
This component obtains data from the beer service and handles the subscription within the template. We declare beers as an observable variable and assign the observable from our service to it.
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { BeerService } from '../../beer.service';
import { Beer } from '../../models/beer';
@Component({
selector: 'app-beer-room',
templateUrl: './beer-room.component.html',
})
export class BeerRoomComponent {
public beers$: Observable<Beer[]>;
constructor(private beerService: BeerService) {
this.beers$ = beerService.beers$;
}
}
In the template, we use the async pipe to await the completion of the subscription.
<div *ngIf="beers$ | async as beers">
<div class="chip" *ngFor="let beer of beers">
<img [src]="beer?.image_url" class="avatar avatar-sm" />
{{ beer.name }}
</div>
</div>
Read more about directives and pipes.
ngIf https://angular.io/api/common/NgIf
ngFor https://angular.io/api/common/NgForOf
Async Pipe https://angular.io/api/common/AsyncPipe
The ResolverRoom Component
This component is quite similar to the beer component. We inject ActivateRoute, as it provides access to the data stored by the resolver in the snapshot, assigning it to the `beer` variable.
The value held in the snapshot gets placed into a variable named beerRouterList.
You'll see how the resolver is configured within the route configuration shortly.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Beer } from '../../models/beer';
@Component({
templateUrl: './resolver-room.component.html',
})
export class ResolverRoomComponent implements OnInit {
beerRouterList: Beer[];
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.beerRouterList = this.route.snapshot.data['beers'];
}
}
Just as in the BeerComponent, we loop over the beer array using the ngFor directive.
<div class="chip" *ngFor="let beer of beerRouterList">
<img [src]="beer?.image_url" class="avatar avatar-sm" />
{{ beer.name }}
</div>
That's completed. The subsequent steps involve creating the resolver and configuring it within the route setup.
The Resolver
This is the main protagonist of the article, the resolver. The BeerResolverService implements the Resolve interface. Acting as a data provider, the router relies on it to resolve during navigation, pausing the component activation until it finishes.
It implements the resolve method. Like the component, we inject beerService and return the observable beers$, updating the return type to match Observable.
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { BeerService } from '../beer.service';
import { Beer } from '../models/beer';
@Injectable()
export class BeerResolverService implements Resolve<Observable<Beer[]>> {
constructor(private beerService: BeerService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<Beer[]> {
return this.beerService.beers$;
}
}
Register the resolver and set up routing
We won't dig into how Angular's router works under the hood — the official docs cover that in detail — but for this app we define two routes.
- The route
homerenders theHomeComponent. - The route
beer-roomrenders theBeerRoomComponent. - The route
resolve-roomalso renders the component, but with a twist: it uses a resolver to fetch the data, stores the result in thebeersvariable, and assigns it to the route snapshot'sdataunder the keybeers— the value comes from the subscription. - A catch-all route redirects any unknown path to
home.
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'beer-room',
component: BeerRoomComponent,
},
{
path: 'resolver-room',
component: ResolverRoomComponent,
resolve: { beers: BeerResolverService },
},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
];
See it in action
You now have two different experiences to compare:
- The plain component route takes you straight into the room, but the beer isn't there yet.
- The resolver route lets you enter the area only once the data is ready.
My take
When the room depends on a single value, I personally reach for the resolver.
But when the component needs several requests to load, I prefer resolving the data inside the component itself — the user starts seeing results sooner.
Which approach serves the user better? Play around with both and see what feels right!
I hope this gives you a clearer sense of when and why to use resolvers. If this helped you out, pass it along!
Photo by Meritt Thomas on Unsplash
