Angular Resolvers: How To Master Them

Angular Resolvers

When working with Angular resolvers and Components, you’ve probably come across situations where you load the data a Component needs when it loads. Though there’s nothing wrong with that approach, you may find yourself with a lot of repetitive code depending upon how your APIs are structured.

For example, let’s say Acme Inc. has many multiple locations across the world; which we will call Branches. Your job is to build a SPA for them and in that SPA you have:

  • @Component() SuperAwesomeView
    • Needs a listing of Branches
  • @Component() AnotherSuperAwesomeView
    • Needs a listing of Branches

So naturally you may write some Component code that looks like this:

@Component({
    selector: 'app-super-awesome-view',
    templateUrl: './super-awesome-view.component.html',
    styleUrls: ['./super-awesome-view.scss']
})
export class SuperAwesomeViewComponent implements OnInit {
    branches$: Observable<Branch[]>;

    constructor(private branchService: BranchService) {}

    ngOnInit(): void {
        this.branches$ = this.branchService.getAll();
    }
}

Pretty simplistic right? The problem that we introduce with this approach is really around request management in Components. Let’s say these Components require other fairly generic resource information.

export class SuperAwesomeViewComponent implements OnInit {
    branches$: Observable<Branch[]>;
    users$: Observable<User[]>;
    providers$: Observable<Providers[]>;

    constructor(private branchService: BranchService,
private userService: UserService,
private providerService: ProviderService) {}

    ngOnInit(): void {
        this.branches$ = this.branchService.getAll();
        this.users$ = this.userService.getAll();
        this.providers$ = this.providerService.getAll();
    }
}

Now our Component has grown pretty quickly, we’ve introduced more variables, Injectables, and requests. Though 100% necessary for Components that need this data, there is a better way to manage and share these resource requests. Enter the Resolve Interface in Angular, which its concrete implementations are commonly referred to as Resolvers.

Resolvers

Angular resolvers are effectively an @Injectable that implements the Resolve Interface with the goal of returning some data model.

interface Resolve<T> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T
}

Though at first glance, you may think “well I was already returning some data in my Component, how is this better?”, the power of the Resolver really comes into play with how Angular uses it.

A Resolver can be added to any Route that you define in your Routing Module, allowing the requests to process during the Router’s navigation lifecycle before your Component is loaded. Some benefits to this approach:

  • When the Component loads, data is preloaded.
  • Component code is more meaningful and less cluttered.
    • More logic geared towards the Components true purpose.
    • Less management around handling loading indicators etc.
  • Code is centralized and modular.
  • Hooking into the Router provides Resolver related events we can listen for.

So let’s explore this a bit and see how we can implement and apply a Resolver, access its data in a Component, and finally provide an indication to the user during navigation that content is loading.

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { BranchService } from '@app/services';
import { Branch } from '@app/models';

@Injectable({ providedIn: 'root' })
export class BranchResolver implements Resolve<Observable<Branch[]>> {
    constructor(private branchService: BranchService) {}
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Branch[]> {
        return this.branchService.getAll();
    }
}

See, easy! In the above example, we marked the class with @Injectable and made it available to the application through its providedIn config property. From there, we implement the Resolve<T> Interface and type it properly; which for us is Resolve<Observable<Branch[]>>. Then implement the resolve function, handing it a reference to the API call we want to make.

Hooking our Angular Resolvers Up

With our Resolver implemented, we can use it in our application. To do so, we need to apply it to any route that needs it. In your Routes array, find the Route that you’d like to apply it do. Within that Route, in addition to the properties like path and component, add one called resolve. This property will take an Object with a property we define and map to the Resolver.

{
   "path":"awesome",
   "component":"SuperAwesomeViewComponent",
   "resolve":{
      "branches":"BranchResolver"
   }
}

Once again, it’s pretty simple right? We’ve quickly built a Resolver and now applied it to our Route. So how do we access the data in our Component?

angular

Accessing Resolver Data

If we hop back to our SuperAwesomeViewComponent example, we will need to amend it a bit.

 export class SuperAwesomeViewComponent implements OnInit {
    branches$: Observable<Branch[]>;

    constructor(private activatedRoute: ActivatedRoute) {}

    ngOnInit(): void {
       const data:Data = this.activatedRoute.snapshot.data;
       this.branches$.next(data.branches);   
    }
}

We’ve swapped out the BranchService for the Angular ActivatedRoute. The ActivatedRoute allows us to access all resolved data and use it in our Component. If you remember that in the Route we added our Resolver to we defined an Object with the data we want to use, the Data object we receive from the ActivatedRoute is that Object.

Router Events and Loading Screen

With data being loaded during the navigation lifecycle and being handed to our Component, we now want to show some indication to the user that we’re loading data. To do this properly, we’ve going to move a bit higher up in the Component tree to the AppComponent. In your SPA, you probably have a root Component like the AppComponent that houses the root router-outlet. This Component will be where we centralize listening for Resolver-based events in the Router and overlaying a loading screen. For example:

export class AppComponent implements OnInit {
  loading: boolean = false;
  constructor(private router: Router) {}

  ngOnInit(): void {
    this.router.events
      .pipe(
        filter(
          event => event instanceof ResolveStart || event instanceof ResolveEnd
        )
      )
      .subscribe((event: RouterEvent) => {
        if (event instanceof ResolveStart) {
          this.loading = true;
        } else if (event instanceof ResolveEnd) {
          this.loading = false;
        }
      });
  }
}

The Router emits two events aimed at the Resolvers, ResolveStart and ResolveEnd. They are pretty straightforward and tell us everything we need to know to show the loading screen. When we receive those events, we toggle a boolean flag to show a loading screen so the user is aware that we are fetching necessary data.

General Thoughts on Resolvers

The above walkthrough is pretty simple and really geared towards an understanding of how they can be used. It’s truly just another useful tool at your disposal when building an Angular application but it isn’t a blanket solution to all scenarios. Below are some thoughts I continually think about while building Angular applications and trying to structure requests properly.

How much can a Resolver Handle?

Resolvers are a great way to preload data, but they really depend on how your APIs are structured and what you expect in return. Are you calling a single API that’s returning a lightweight or heavy model? Or are you calling multiple APIs and performing transformations on the returned data? Regardless of which approach, what’s the user’s experience while it’s resolving? All of these questions are answered on a per project basis but should always be considered.

Error handling in a Resolver?

If we’re requesting data while navigating, you need to be smart about how you handle situations where an API is unreachable or there’s an exception in your API. Connectivity issues are common, whether they be directly to the API or to another service or database that API may use. Is it better to handle the user experience for this in the Component with the ability to reload or is a blanket error page good enough?

What kind of data should be retrieved?

Sure, simple resource data is a good candidate. It’s even better if that resource data tends to be static and can be cached with little overhead towards when to invalidate that cache. But what about things like tabular data?

Is a Resolver a good way to preload the first page of a table so when the Component loads it’s readily available? It could be. Depends on how generic you can make that code and if that code is truly needed in multiple places.

What about form data? I think Resolvers are an excellent way to pulling in data like saved forms and pre-populating those fields. Could it get hairy? Yeah, probably if your pages has a nasty form (which I’ve seen plenty), but it really comes down to how that form data is stored. Is it a single model or composed from different calls?

Conclusion

Resolvers are a super handy feature in Angular. They are simply built, reusable, and makes testing your code a bit simpler. Lastly, I’m honestly not sure most Angular developers are aware that this feature exists, so try implementing one on your project and it could make you look like an Angular guru! If you do, let me know how things went and if you have another angle to consider!

 

To learn more about Angular route Resolvers or more coding questions, contact our team at Rōnin today! 

About Rōnin Consulting – Rōnin Consulting provides software engineering and systems integration services for healthcare, financial services, distribution, technology, and other business lines. Services include custom software development and architecture, cloud and hybrid implementations, business analysis, data analysis, and project management for a range of clients from the Fortune 500 to rapidly evolving startups. For more information, please contact us today.

Author:
Brian Weiss is a UI/UX Architect hellbent on designing and developing, meaningful, human-focused applications. He's also hellbent on finding the best chocolate chip cookie this world has to offer. When he's not developing software or eating cookies, he's probably watching anime and collecting Funko Pops.