Skip to content
Modern conference room with six high-back chairs around a table in a bright office with floor-to-ceiling windows.

Angular

Load route-based data with resolver

Björn Möllers

Copy Link

Link copied

Resolver – load simple data

The resolver loads data when a route is opened. The route parameters can be used. For the route \product\42, a resolver can load the product details with ID 42. The resolver ensures that all dynamic data is available when the component is initialized. We looked at resolvers in episode 15 of the Happy Angular Podcast. Listen to it. The advantages and disadvantages briefly summarized:

Advantages:

  • Good structuring of the program code
  • Distribution to several resolvers possible
  • Reusability of the resolver for other components

Disadvantages:

  • Latency, as all content is loaded before the component is initialized (partial construction of a list is not possible)
  • Content that is not in the viewing area may be loaded
  • Resolvers do not cache the data when the route is reopened (navigation to another route and back).
  • Error handling

We also look at how the disadvantages can be overcome.
But first, let’s look at the basic use of a resolver.

Basic use

Let’s take a look at a resolver for loading a list as an example. A resolver is a service that is bound to a route. That is why we first look at the routing configuration. We use resolve to specify the property under which the resolver should later be accessible. In this example, the ListResolver is available under the property list.

const routes: Routes = [
  {
    path: 'overview', component: OverviewComponent, resolve: {
      list: ListResolver
    }
  },
  {path: '', pathMatch: 'full', redirectTo: 'overview'},
  {path: '**', component: PageNotFoundComponent}
];

@NgModule({
  imports: [
    HttpClientModule,
    RouterModule.forRoot(routes)
  ],
  exports: [RouterModule],
  providers: [
    ListResolver,
    ItemResolver,
    DelayResolver,
    PartialResolver,
    MatrjoschkaResolver
  ]
})
export class AppRoutingModule {
}

The resolver itself requires the interface Resolve and this interface is typed – via generics. The program code below shows us that the resolver returns CountryListItem[]. In addition, the method resolve loads the data from a backend and transforms the result into the desired structure. A simple type of error handling can be found in the example: If an error occurs, the user is redirected to an error page.

@Injectable()
export class ListResolver implements Resolve {

  constructor(private http: HttpClient, private router: Router) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
    return this.http.get('https://restcountries.eu/rest/v2/all?fields=alpha3Code;name;population').pipe(
      map((list: Object[]) => list.map(item => CountryListItem.fromJson(item)))
    ).pipe(
      catchError(error => {
        this.router.navigateByUrl('/page-not-found');
        return of(undefined);
      })
    );
  }
}

An entity class is available for managing the data in the Angular application. The fromJson method simplifies the transformation of the API response. You learned about this in the previous section.

export class CountryListItem {
  constructor(public id: string,
              public name: string,
              public population: number) {

  }

  static fromJson(obj: Object): CountryListItem {
    return new CountryListItem(obj['alpha3Code'], obj['name'], obj['population']);
  }
}

The dependency ActivatedRoute is required for display in a component. We have opted for the visibility public, as we will access the resolver data directly in the template.

@Component({
  selector: 'app-overview',
  templateUrl: './overview.component.html',
  styleUrls: ['./overview.component.scss']
})
export class OverviewComponent {

  displayedColumns: string[] = ['name', 'population'];

  constructor(public activatedRoute: ActivatedRoute) {
  }

}

The question arises: How can I access the data?
The ActivatedRoute presents us with the data as Observable. An Observable can be resolved in the template with the async pipe. We have provided our data in the routing configuration at list, which we call up here. At this point, we use Material Design or the Material Design table to display the data. The whole table looks like this:

Overcoming disadvantages

However, the rudimentary use has some disadvantages. The route is only resolved once all the data has been loaded by the resolver and only then is the component displayed to the user. This time delay is annoying and also noticeable with low latencies. To make matters worse, the popular “piece-by-piece” loading of data is not possible with this technology. It has the same effect as if everything was loaded at once: The component is not displayed until the list is complete. With resolvers, data that is not part of the DOM is also loaded by default and therefore unnecessarily increases the initial latency. And the list of disadvantages does not end there: the loaded data is not cached. After exiting the route and reopening it, the data is loaded again. The user has to wait for the application again. Error handling? Only rudimentary. All in all, resolvers have many disadvantages and yet they are so useful. With a small change, we can easily solve many of the disadvantages. Only for a few we need more effort. Do you know the Russian Matryoshka figures? There is another figure inside each one. And the resolver gets an upgrade exactly according to this concept. Instead of loading the data, we load a Observable. This allows the component to be displayed immediately. The content is reloaded. The DOM tree used defines which data is required. We can also design our own error handling – as with network failures. And caching is possible with this small change. Let’s take a look at an example.

Matryoshka Resolver

In this scenario, there is no such thing as a perfect world: request data, load and you’re done – not this time! 20 data records are loaded – each with a delay of 50 ms. And the last data record always causes an error to test our error handling.
We have specified that if an error occurs, the data is attempted to be loaded again. To rule out temporary situations and not overload the server, there is an increasing time delay between attempts. The first retry starts after 2 seconds, the second after 4 seconds, the third and last retry starts after 6 seconds after an error occurs. To make the example not too complex, we load a list of numbers as data records.

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router';
import {interval, Observable, of} from 'rxjs';
import {catchError, map, retryWhen, scan, take} from 'rxjs/operators';
import {genericRetryStrategy} from './generic-retry-strategy';

@Injectable()
export class MatrjoschkaResolver implements Resolve<observable> {

  constructor(private router: Router) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<observable> {
    const observable = interval(50).pipe(
      take(20),
      map(x => {
          if (x === 19) {
            throw x;
          } else {
            return x;
          }
        }
      ),
      scan((acc: number[], x: number) => {
        return acc.concat([x]);
      }, []),
      retryWhen(genericRetryStrategy({
          scalingDuration: 2000,
          excludedStatusCodes: [500]
        })
      ),
      catchError(error => {
          this.router.navigateByUrl('/page-not-found');
          return of(error);
        }
      )
    );
    return of(observable);
  }
}
</observable</observable

At this point, I would like to point out the interface Resolve<Observable<number[]>> and the return type of the resolve method. This small difference allows us to use the resolvers in many situations. The presentation of the data in the template is also slightly different, but not much. It is an extension of the above example and at the same time a simplification. The snapshot is only a snapshot. However, as it does not contain the data directly, but only the reference to the Observable, this is completely sufficient. item is the access option to the resolver – see route configuration. To get the data from the asynchronous structure of Observable, we use the async pipe.

An animation can be displayed during loading. The data is not cached in this example – although this would be possible. In the next part, we will look at how we can load data with Redux. You can find the complete program code at https://github.com/dornsebastian/angular-resolver. You can find the Happy Angular Podcast about the resolver here.


Angular
External data
Resolver
routing