Intro

Rxjs provides a number of mechanisms for caching HTTP request results. One good option is BehaviorSubject, which emits a default value when first subscribed to (before the HTTP response is received), and will subsequently emit the latest result to all early and late subscribers. This is perfect for caching results in a service, and subscribing to that service from different components. Subscribing to a BehaviorSubject combines the traditional steps of retrieving initial values, then listening for updates; a pattern commonly used to avoid timing issues with initial data retrieval delays

One of the variants of Subjects is the BehaviorSubject, which has a notion of "the current value". It stores the latest value emitted to its consumers, and whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject -- ReactiveX Manual

Route Resolvers

Resolvers in Angular provide a way of pre-fetching data before navigation to a route. When specifying a route we set the resolve property to an object with a key-value pair with a value that implements the @angular/router/Resolve interface

{
    path: 'data',
    component: DataPageComponent,
    resolve: { resolvedData: DataResolverService }
}

We choose the key name ourselves e.g. resolvedData, and reference this property when the component is loading

Route Resolve Interface

The resolver service we create must implement the resolve function of the Resolve interface. This function should return either a Promise, Observable, or value:T (of any Type). When returning an Observable, it must complete for the navigation to continue. In cases where we are working with a single observable, a straightforward map() will usually suffice. If there are multiple data sources to resolve/pre-fetch, we need to do a little bit more

BehaviorSubject Observables

For this example we will create a mock data API service with two BehaviourSubject observables that we will subscribe to. The following is a class that replicates a service making 2 HTTP requests. We will use this as our API service from which we'll pre-fetch the data to be resolved before navigating to the route

data-api.service.ts

export class DataApiService implements AbstractDataApiService {

  private clientsBehaviorSubject: BehaviorSubject<IClient[]> = new BehaviorSubject(undefined);
  private accountsBehaviorSubject: BehaviorSubject<IAccount[]> = new BehaviorSubject(undefined);

  private dataInitialized = false;

  constructor() { }

  getClients(): Observable<IClient[]> {
    if (!this.dataInitialized) {
      this.initData();
    }
    return this.clientsBehaviorSubject.asObservable();
  }

  getAccounts(): Observable<IAccount[]> {
    if (!this.dataInitialized) {
      this.initData();
    }
    return this.accountsBehaviorSubject.asObservable();
  }

  // This replicates XHR calls to the client and account API with a
  // delayed response for each
  private initData() {
    this.dataInitialized = true;

    timer(200)
      .pipe(
        map( value => {
          const cl1: IClient = { id: 'Client 1'};
          const cl2: IClient = { id: 'Client 2'};
          return [cl1, cl2];
        })
      )
      .subscribe( value => {
        this.clientsBehaviorSubject.next(value);
      });

    timer(300)
      .pipe(
        map( value => {
          const ac1: IAccount = { id: 'Account 1'};
          const ac2: IAccount = { id: 'Account 2'};
          return [ac1, ac2];
        })
      )
      .subscribe( value => {
        this.accountsBehaviorSubject.next(value);
      });
  }

}

CombineLatest

The resolver class, data-resolver.service.ts will need to map the results of both BehaviorSubject observables from the data-api.service.ts. Keeping in mind that a BehaviorSubject can return an initial default value (in our case undefined) we will need to add a filter that only maps the result when both BehaviorSubjects have emitted their latest 'truthy' values. Finally, after the filter and map, we call first(), which completes the combineLatest observable - and allows navigation to occur

combineLatest: This operator is best used when you have multiple, long-lived observables that rely on each other for some calculation or determination -- Learn Rxjs

data-resolver.service.ts

export interface IResolvedData {
  clients: IClient[];
  accounts: IAccount[];
}

@Injectable()
export class DataResolverService implements Resolve<IResolvedData> {

  constructor(private dataServcie: AbstractDataApiService) {

  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IResolvedData> {

    const clients$ = this.dataServcie.getClients();
    const accounts$ = this.dataServcie.getAccounts();

    const combine$ = combineLatest(clients$, accounts$,
      (clients, accounts) => {
        return <IResolvedData>{ clients, accounts };
      })
      .pipe(
        filter((value, index) => {
          console.log('Resolver Filter: ', value);
          return (value.clients && value.accounts) ? true : false;
        }),
        map(value => {
          console.log('Resolver Map & Complete:', value);
          return value;
        }),
        first()
      );
    return combine$;
  }

}

StackBlitz Demo

Click on the Preview tab to see the route resolver in actions. The console panel at the bottom of the Perview tab displays the progress of the resolve() function

⚡️StackBlitz is an incredible online editor based on VSCode. It can be used to run and edit Angular CLI generated GitHub repositories directly in the browser using the following url sequence stackblitz.com/github/{GH_USERNAME}/{REPO_NAME} - really taking JavaScript editing on the web to the next level!

Brian Bishop

Dublin, Ireland
v7.2.15+sha.e7897e1