Route Resolvers
Esta página aún no está disponible en tu idioma.
Overview
Section titled “Overview”Route resolvers are a powerful Angular feature that allows you to load data before a route is activated. Rather than loading data inside a component’s ngOnInit, resolvers fetch required data first, ensuring that components only render when all their dependencies are satisfied.
This guide explores route resolvers comprehensively, covering when they’re valuable, how to implement them correctly, and when modern alternatives might be better suited.
Why Resolvers Matter
Section titled “Why Resolvers Matter”Guaranteed Data Availability
Section titled “Guaranteed Data Availability”When a resolver completes, the component is guaranteed to have the data it needs. No need for loading states in the component—the data is already there.
Better User Experience
Section titled “Better User Experience”Users don’t navigate to a route and see a loading spinner. Instead, they wait for the data to load and then navigate to a fully-rendered component.
Cleaner Components
Section titled “Cleaner Components”Components can be simpler because they don’t need to handle initial data loading logic. Data flows directly from the resolver.
Centralized Logic
Section titled “Centralized Logic”Data loading logic lives in one place (the resolver) rather than scattered across multiple components.
Understanding Resolvers
Section titled “Understanding Resolvers”What is a Resolver?
Section titled “What is a Resolver?”A resolver is a service that implements the Resolve interface. It fetches data and returns an Observable, Promise, or value that Angular waits for before activating the route.
export interface Resolve<T> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T;}How Resolvers Work
Section titled “How Resolvers Work”- User attempts to navigate to a route with a resolver
- Resolver’s
resolve()method is called - Angular waits for the Observable/Promise to complete
- Data is passed to the component via
ActivatedRoute - Component renders with data already loaded
Implementation Guide
Section titled “Implementation Guide”Basic Resolver Example
Section titled “Basic Resolver Example”import { Injectable } from '@angular/core';import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';import { Observable } from 'rxjs';import { UserService } from './user.service';
export interface User { id: number; name: string; email: string;}
@Injectable({ providedIn: 'root'})export class UserResolver implements Resolve<User> { constructor(private userService: UserService) {}
resolve( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<User> { const userId = route.paramMap.get('id'); return this.userService.getUserById(userId!); }}Registering Resolvers in Routes
Section titled “Registering Resolvers in Routes”Module-Based Approach (NgModules):
import { NgModule } from '@angular/core';import { RouterModule, Routes } from '@angular/router';import { UserDetailComponent } from './user-detail.component';import { UserResolver } from './user.resolver';
const routes: Routes = [ { path: ':id', component: UserDetailComponent, resolve: { user: UserResolver } }];
@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule]})export class UserRoutingModule { }Standalone Components Approach:
import { Routes } from '@angular/router';import { UserDetailComponent } from './user-detail.component';import { UserResolver } from './user.resolver';
export const USER_ROUTES: Routes = [ { path: ':id', component: UserDetailComponent, resolve: { user: UserResolver } }];Accessing Resolved Data in Components
Section titled “Accessing Resolved Data in Components”import { Component, OnInit } from '@angular/core';import { ActivatedRoute } from '@angular/router';
@Component({ selector: 'app-user-detail', template: ` <div class="user-detail"> <h1>{{ user.name }}</h1> <p>Email: {{ user.email }}</p> <p>ID: {{ user.id }}</p> </div> `})export class UserDetailComponent implements OnInit { user: User | null = null;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void { this.route.data.subscribe(data => { this.user = data['user']; }); }}With Signals (Modern Approach):
import { Component, OnInit, signal } from '@angular/core';import { ActivatedRoute } from '@angular/router';
@Component({ selector: 'app-user-detail', standalone: true, template: ` <div class="user-detail"> <h1>{{ user().name }}</h1> <p>Email: {{ user().email }}</p> <p>ID: {{ user().id }}</p> </div> `})export class UserDetailComponent implements OnInit { user = signal<User | null>(null);
constructor(private route: ActivatedRoute) {}
ngOnInit(): void { this.route.data.subscribe(data => { this.user.set(data['user']); }); }}Multiple Resolvers
Section titled “Multiple Resolvers”export const routes: Routes = [ { path: 'dashboard', component: DashboardComponent, resolve: { user: UserResolver, stats: StatsResolver, notifications: NotificationsResolver } }];Accessing multiple resolved values:
ngOnInit(): void { this.route.data.subscribe(data => { this.user = data['user']; this.stats = data['stats']; this.notifications = data['notifications']; });}Error Handling
Section titled “Error Handling”Handling Resolver Errors
Section titled “Handling Resolver Errors”@Injectable({ providedIn: 'root'})export class UserResolver implements Resolve<User> { constructor( private userService: UserService, private router: Router ) {}
resolve(route: ActivatedRouteSnapshot): Observable<User> { const userId = route.paramMap.get('id');
return this.userService.getUserById(userId!).pipe( catchError(() => { this.router.navigate(['/404']); return EMPTY; }) ); }}Using Route Guards with Resolvers
Section titled “Using Route Guards with Resolvers”@Injectable({ providedIn: 'root'})export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {}
canActivate(): Observable<boolean> { return this.authService.isAuthenticated$.pipe( take(1), tap(isAuth => { if (!isAuth) { this.router.navigate(['/login']); } }) ); }}{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuard], resolve: { profile: ProfileResolver }}Best Practices
Section titled “Best Practices”✅ DO’s
Section titled “✅ DO’s”Do use resolvers for critical data
{ path: ':id', component: UserDetailComponent, resolve: { user: UserResolver }}Do handle resolver errors gracefully
resolve(): Observable<User> { return this.userService.getUser().pipe( catchError(() => { this.router.navigate(['/404']); return EMPTY; }) );}Do keep resolvers focused
@Injectable({ providedIn: 'root' })export class UserResolver implements Resolve<User> { constructor(private userService: UserService) {}
resolve(route: ActivatedRouteSnapshot): Observable<User> { return this.userService.getUserById(route.paramMap.get('id')!); }}❌ DON’Ts
Section titled “❌ DON’Ts”Don’t use resolvers for optional data
{ path: 'product/:id', component: ProductComponent, resolve: { recommendations: RecommendationsResolver }}Don’t create slow resolvers
resolve(): Observable<Data> { return this.api.getData1().pipe( mergeMap(data1 => this.api.getData2(data1.id).pipe( mergeMap(data2 => this.api.getData3(data2.id)) ) ) );}
resolve(): Observable<Data> { return forkJoin([ this.api.getData1(), this.api.getData2(), this.api.getData3() ]);}Don’t forget to unsubscribe when using Observables
export class Component implements OnInit, OnDestroy { private destroy$ = new Subject<void>();
ngOnInit(): void { this.route.data.pipe( takeUntil(this.destroy$) ).subscribe(data => { this.user = data['user']; }); }
ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); }}Resolvers vs. Other Approaches
Section titled “Resolvers vs. Other Approaches”Resolvers vs. Direct Component Loading
Section titled “Resolvers vs. Direct Component Loading”Resolver Approach:
{ path: 'user/:id', component: UserComponent, resolve: { user: UserResolver }}
ngOnInit(): void { this.route.data.subscribe(data => this.user = data['user']);}Direct Loading in Component:
{ path: 'user/:id', component: UserComponent}
ngOnInit(): void { this.userService.getUser(id).subscribe(user => this.user = user);}Trade-offs:
| Aspect | Resolver | Direct Loading |
|---|---|---|
| User Experience | Wait, then view fully loaded | Immediate navigation, loading spinner |
| Complexity | Routing config | Component logic |
| Testing | Easier to mock data | Requires more mocking |
| Performance | Delays navigation | Faster perceived performance |
Resolvers vs. Signals
Section titled “Resolvers vs. Signals”Modern Angular with signals offers an alternative:
{ path: ':id', component: UserComponent, resolve: { user: UserResolver }}
export class UserComponent implements OnInit { userId = signal<string | null>(null); user = signal<User | null>(null);
constructor(private route: ActivatedRoute, private userService: UserService) { effect(() => { const id = this.userId(); if (id) { this.userService.getUserById(id).subscribe( user => this.user.set(user) ); } }); }
ngOnInit(): void { this.userId.set(this.route.snapshot.paramMap.get('id')); }}Modern Alternatives
Section titled “Modern Alternatives”Using RxJS in Components
Section titled “Using RxJS in Components”export class UserComponent implements OnInit { user$ = new Observable<User>();
constructor( private route: ActivatedRoute, private userService: UserService ) {}
ngOnInit(): void { this.user$ = this.route.paramMap.pipe( switchMap(params => this.userService.getUserById(params.get('id')!) ) ); }}
@if (user$ | async as user) { <h1>{{ user.name }}</h1>}Using Async Pipe
Section titled “Using Async Pipe”export class UserComponent { user$: Observable<User>;
constructor( private route: ActivatedRoute, private userService: UserService ) { this.user$ = this.route.paramMap.pipe( switchMap(params => this.userService.getUserById(params.get('id')!) ) ); }}
<div *ngIf="user$ | async as user"> <h1>{{ user.name }}</h1></div>When to Use Resolvers (Decision Guide)
Section titled “When to Use Resolvers (Decision Guide)”✅ Use Resolvers When:
Section titled “✅ Use Resolvers When:”- Data is required for component to render meaningfully
- User must have the data to interact with the page (e.g., user details page)
- Better UX with waiting than showing loading spinners
- Multiple routes need the same data
- Want to keep components simple and data-focused
❌ Don’t Use Resolvers When:
Section titled “❌ Don’t Use Resolvers When:”- Data is optional (recommendations, related items, etc.)
- Data loads quickly (< 100ms)
- You need perceived performance (fast route transitions)
- Data frequently changes (live updates, real-time data)
- Component has complex loading logic (pagination, filters)
Conclusion
Section titled “Conclusion”Route resolvers are a powerful tool for managing data loading in Angular applications. They work best for critical data that must be available before a route activates. However, they’re not always the right choice.
Key Takeaways:
- Use resolvers for essential data that makes sense to load before navigation
- Combine with error handling to gracefully manage failures
- Consider modern alternatives like signals and async pipes
- Keep resolvers simple and focused
- Measure and optimize based on actual user experience
The best approach depends on your specific use case, data requirements, and desired user experience.
References
Section titled “References”This guide is based on and inspired by: