Pre-loading with Route Guards

Angular: The Full Gamut Edition

Charlie Greenman
September 13, 2020
5 min read
razroo image

In Angular, a RouteGuard is an interface that can be implemented to determine if a given route request should be fulfilled or not. The core purpose of a RouteGuard is to protect a route by applying authorization to it. However, we can use a Route Guard for another purpose: pre-loading data for a view.

Motiviation

The reason for doing this is to change where in the request process the loading of data happens. Instead of determining the route, rendering view, and then loading data, we find the route, load the data we need, and then render the view with the data already in hand. The question as to why we want to do this?

We would change the order from A to B.

We want to change the data loading order from Figrue 1, to Figure 2.

How It Works

In addition to providing hooks for determining authorization, RouteGuards provide a means for pre-fetching and caching data in the store. This is an effect of the place that Route Guards occupy in the processing of requests. Here's a look at a very simple Route Guard:

The canActivate() method is called by Angular to determine if the route in question is allowed, based on the boolean return value. If we were really using it for authorization, we could call out to a AuthService to check a token or similar.

This simple version always allows the route to be activated.

// Listing 1
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';

@Injectable()
export class StoreLoadingGuardService implements CanActivate {
  constructor(public auth: AuthService, public router: Router) {}

  canActivate(): boolean {
    return true;
  }

}

If we were concerned with authorization here, the .canActivate() method would reach out to an authentication service to make it's determination. For our purposes, though, lets use this code in Listing 1 to see how to plug the Route Guard into our app architecture.

  // Listing 2
import { Routes, CanActivate } from '@angular/router';
import { ExampleComponent } from './example/example.component';
import {
  StoreLoadingGuardService as LoadingGuard
} from './auth/loading-guard.service';

export const ROUTES: Routes = [
  //...
  {
    path: 'example',
    component: ExampleComponent,
    canActivate: [LoadingGuard]
  }
  //...
];

What Listing 2 says, is: when the route example is called, invoke the LoadingGuard.canActivate() method we defined before. Right now, all that will do is allow the route with a default return value of true. However, we can do something more interesting by pre-loading our store.

The Action

Pre-loading data depends on the store being a central and persistent object that holds application state. When modifying this state, we use ngrx Actions, a la Redux. Below in Listing 3 is a simple Action for loading data. This simple action allows for a load action and a load success action for a Song data type. (Yes, that is correct, we are pre-tending that we are building a music application, right now.)

export const LOAD_ALL_SONGS = '[Song] Load All Songs';
export const ALL_SONGS_LOADED = '[Song] All Songs Loaded';

export class LoadAllSongs implements Action {
  readonly type = LOAD_ALL_SONGS;
  constructor(public payload?: any) { }
}
export class AllSongsLoaded implements Action {
  readonly type = ALL_SONGS_LOADED;
  constructor(public payload: string[]) { }
}

The Store

Our central state might look like the following:

  export interface State {
  songs: string[];
}

export const initialState: State = {
  songs: [];
};

export function reducer(state = initialState, action: song.Actions) {
  switch (action.type) {
    case song.LOAD_ALL_SUCCESS: {
      return Object.assign({}, state, {
        songs: action.payload
      });
    }

    // ...
  }
}

This reducer simply applies the loaded songs to the state upon a successful load. We will rely on this reducer to merge the data returned by the action into the state.

The Effect

In the ngrx/store style pattern, we use Effects to handle async calls:

@Effect()
loadAll$: Observable = this.actions$
.ofType(song.LOAD_ALL)
.switchMap(() => {
  return this.service.getAll()
  .map(songs => new song.LoadAllAction(songs))
  .catch(() => of(new song.LoadAllFailAction()));
});

This is a simple effect that relies on a service (that has been injected) to retrieve the set of songs, or invoke the LoadAllFailAction action if an error is thrown.

Our store is in place. We can now focus on our new updated route guard.

Modified CanActivate

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
  const loadedSongs = this.store.select(fromRoot.getSongs)
    .map(songs => songs.length > 0);

  loadedSongs
    .take(1)
    .filter(loaded => !loaded)
    .map(() => new song.LoadAllAction())
    .subscribe(this.store);

  return loadedSongs
    .take(1);
}

This determines if the desired data is already present in the store. If it's not, it loads that data, and then allows the route to proceed where the view will have access to the data loaded into the central state.

Unpacking

We use a store selector to pull the songs that are already present in this.store.select(fromRoot.getSongs) and of non-zero length. This we save in the loadedSongs const.

Next, we use take(1) to grab the first item in the dataset, and then check if it's falsey with filter(loaded => !loaded) - the net result being to run the .map() call on an empty dataset if the source contains nothing. The net result is to skip loading the data in the next call if there is already data present.

If the dataset is empty, then we map a call to the song loading service, and subscribe the store to it's result, thereby loading the data into the store. Finally, we unsubscribe from the source.

Subscribe to the Razroo Angular Newsletter!

Razroo takes pride in it's Angular newsletter, and we really pour heart and soul into it. Pass along your e-mail to recieve it in the mail. Our commitment, is to keep you up to date with the latest in Angular, so you don't have to.

More articles similar to this

footer

Razroo is committed towards contributing to open source. Take the pledge towards open source by tweeting, #itaketherazroopledge to @_Razroo on twitter. One of our associates will get back to you and set you up with an open source project to work on.