Cómo usar Resolvers en Angular

Cuando vamos a una fiesta, me encanta tener todas las cervezas listas para tomar una, pero tener que esperar a que las saquen de la nevera a la mesa no es una buena experiencia.
Lo mismo les pasa a los usuarios con nuestras aplicaciones en Angular: mostramos el componente, pero los datos todavía no están listos o la petición HTTP tarda y ellos se quedan esperando.
Una buena solución es mostrar un indicador de carga hasta recibir la respuesta. Pero, ¿qué pasa si mis usuarios quieren tener las cervezas listas desde el inicio? No hay problema, porque Angular tiene una solución: los Resolvers.
Los Resolvers nos ayudan a precargar datos antes de que el enrutador termine la navegación, iniciando la transición entre componentes y guardando esa información.
Son muy útiles para evitar mostrar un componente vacío y asegurar que tengamos nuestros datos listos antes de pasar a la vista.
La fiesta de Angular
Vamos a poner a prueba el resolver usando un servicio y construyendo una pequeña aplicación para mostrar una lista de cervezas desde la API https://api.punkapi.com/v2/beers.
La aplicación tiene dos rutas, ComponentRoom y ResolverRoom, y cada una ofrece una experiencia de usuario diferente:
-
ComponentRoom usa el pipe async para obtener los datos desde el servicio.
-
ResolverRoom usa un resolver para precargar los datos y el componente accede a ellos a través de
route.snapshot.data.
¿Qué necesitamos hacer?
Iremos paso a paso:
-
Crear una interfaz para mapear la respuesta de la API.
-
Proveer un servicio de cervezas para solicitar los datos y devolver un observable con el resultado.
-
Crear tres componentes: BeerRoom, ResolverRoom y HomeComponent.
-
Implementar el resolver.
-
Registrar el resolver y definir las rutas de la aplicación.
También incluiremos otros elementos clave como Router, ActivatedRoute, Observable, etc. ¡Vamos a ponerlo a funcionar!
El código fuente está en https://github.com/danywalls/how-prefech-with-resolver
El servicio de cervezas
Creamos una interfaz llamada Beer y un servicio BeerService para obtener los datos de la API.
La interfaz Beer define algunas de las propiedades que recibimos en la respuesta de la API.
export interface Beer {
id: number;
name: string;
tagline: string;
first_brewed: string;
description: string;
image_url: string;
}BeerService requiere inyectar HttpClient, hacer la petición a la API y apoyarse en RxJS para devolver un observable con un arreglo de Beer.
Importamos HttpClient y el decorador Injectable. Luego creamos el método getBeers para retornar el resultado de la consulta a https://api.punkapi.com/v2/beers. Además, usamos el operador delay para simular una respuesta lenta de unos segundos:
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));
}
}Puedes leer más sobre operadores y servicios en: Delay Operator https://www.learnrxjs.io/learn-rxjs/operators/utility/delay y Services https://angular.io/tutorial/toh-pt4
El componente Home
La página de inicio tiene dos enlaces para acceder a las rutas beer-room y resolver-room, utilizando la directiva routerLink.
<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>Más sobre routerLink en https://angular.io/api/router/RouterLink
El componente BeerRoom
Este componente obtiene los datos desde el servicio de cervezas y resuelve la suscripción directamente en la plantilla. Declaramos la variable beers$ como un observable y le asignamos el observable de nuestro servicio:
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$;
}
}Dentro de la plantilla, usamos el pipe async para esperar a que termine la suscripción:
<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>Revisa más sobre directivas y pipes en: ngIf https://angular.io/api/common/NgIf, ngFor https://angular.io/api/common/NgForOf y Pipe Async https://angular.io/api/common/AsyncPipe
El componente ResolverRoom
A diferencia del componente anterior, aquí inyectamos ActivatedRoute, que nos proporciona los datos guardados en el snapshot por el resolver.
Guardamos el valor del snapshot en la variable beerRouterList:
Más adelante veremos cómo configurar el resolver en la definición de rutas.
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'];
}
}Al igual que en BeerRoomComponent, iteramos sobre el arreglo de cervezas con la directiva ngFor:
<div class="chip" *ngFor="let beer of beerRouterList">
<img [src]="beer?.image_url" class="avatar avatar-sm" />
{{ beer.name }}
</div>Listo. Los siguientes pasos consisten en crear el resolver y configurarlo en las rutas.
El Resolver
La pieza fundamental es el resolver. BeerResolverService implementa la interfaz Resolve. El resolver funciona como un proveedor de datos que el enrutador ejecuta durante el proceso de navegación, esperando a que se complete antes de activar el componente.
Implementamos el método resolve. Al igual que en el componente, inyectamos BeerService y retornamos el observable beers$, asegurando que el tipo de retorno coincida con Observable<Beer[]>:
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$;
}
}Registrar el resolver y crear las rutas
No entraremos en detalles sobre el funcionamiento interno del router en Angular (puedes consultar la documentación oficial para ver más detalles), pero aquí definimos las rutas de nuestra aplicación:
-
La ruta
homecargaHomeComponent. -
La ruta
beer-roomcargaBeerRoomComponent. -
La ruta
resolver-roomcarga el componente con un detalle clave: usa la propiedadresolvepara obtener los datos provistos por el resolver y guardarlos enroute.snapshot.databajo la clavebeerscon el valor emitido por la suscripción. -
La ruta por defecto (
'') redirige cualquier petición haciahome.
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' },
];La experiencia en acción
Listo, tenemos dos experiencias de usuario:
-
En ComponentRoom entras a la vista de inmediato, pero las cervezas aún no están listas (el usuario ve la pantalla vacía o esperando).
-
En ResolverRoom el resolver te permite pasar a la vista únicamente cuando los datos ya están listos.
Mi opinión personal
Si tu vista solo necesita cargar un único valor o recurso principal, me gusta usar el resolver.
Por el contrario, si mi componente realiza múltiples peticiones HTTP independientes, prefiero manejar los datos dentro del propio componente. De esta forma, el usuario empieza a recibir información de inmediato mientras el resto termina de cargar.
¿Qué opción crees que es mejor para tus usuarios? ¡Prueba ambas y evalúa la experiencia!
Espero que este artículo te sirva de ayuda para entender cómo y cuándo usar resolvers. Si te gustó, ¡compártelo!
Foto por Meritt Thomas en Unsplash
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.