State Aware Resolver - Angular Meets Redux

Resolving the State - Redux meets Angular

At SpotDraft, we absolutely love using Redux with our Angular app, and many pieces of the state are truly global i.e. they are required to configure different pieces of the application and directly read in different components.

When do we load the data into the state?

Many components directly access the state without worrying about how that state was set, so when exactly was the data loaded into the state?

Since our state was simple, we added these requests to the login action, the user logged in, we loaded a bunch of data and this approach worked well… for some time.

As the features increased, it became very clear that not all the data is required to be loaded at all times and different users (based on features available), require different data to be pre-loaded. We needed a better way to declare data dependencies so that data is loaded into the state as and when it's actually required.

Redux meets Resolvers

If you use Angular to power your frontend, chances are that you have used Resolvers at some point.

Resolvers are used to pre-load data required by a route. This means that when the user navigates to a route with a resolver, the component on the route is not initialised till the resolvers have loaded the data.

This is what a resolver configuration looks like -


// app.routes.ts
{
  path:"/dashboard",
  component: DashboardComponent,
  resolve: {
      user: CurrentUserResolver,
      permissions: CurrentUserPermissionResolver
  }
}

So, resolvers take care of ensuring that a page is rendered only after the data it absolutely needs is loaded, that sounds helpful, only if there was a way to connect this to our state so that we can declare which elements of the state does this route depend on.

To bridge this gap, we came up with a concept of a StateAwareResovler, a fancy name for a fairly simple concept.

A StateAwareResolver, is basically a resolver that works like this -

  1. Angular calls the resolve method of the Resolver.
  2. It checks if a certain value is present in the state and is valid.
  3. If (2) is true, then resolve immediately, i.e. the component can now be rendered.
  4. If (2) is false, then call the API that loads the state, set the state and resolve.

Advantages

  • As state is global, once something is loaded, any subsequent routes that depend on that exact piece will resolve immediately.
  • The declarative approach really helps with tracking which routes consume which part of the global state.

Let's dive into the code!

Angular’s resolvers are interfaces that expose a resolve method. This method can return an Object, or they can return an Observable that emits the object.

The structure of the a StateAwareResolver looks something like -


export abstract class StateAwareResolver<T> implements Resolve<T> {
  constructor(
    private selectorFn: (state: IAppState) => T,
    private ngRedux: NgRedux<IAppState>,
    private validatorFn: (data: T, route: ActivatedRouteSnapshot) => boolean
  ) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<T> {
    // get the entire state
    const store = this.ngRedux.getState();
    // the `selectorFn` is a regular function that returns a specific piece
    // of state
    const data = this.selectorFn(store);
    // the `validatorFn` determines if the state is still valid.
    if (this.validatorFn(data, this.route)) {
      return of(data);
    }
    const loadState = this.createLoadStateObservable(
      route.params,
      route.queryParams,
      store
    );
    return loadState;
  }

  private createLoadStateObservable(
    params: Params,
    queryParams: Params,
    state: IAppState
  ) {
    return this.load(params, queryParams, state).pipe(
      take(1),
      tap((value) => {
        this.setInState(value);
      })
    );
  }

  /**
   * Override this method to return an observable that loads the value.
   */
  protected abstract load(
    params: Params,
    queryParams: Params,
    state: IAppState
  ): Observable<T> {}

  /**
   * Override this method to set it in the state
   */
  protected abstract setInState(value: T) {}
}


Note: The NgRedux service is provided by the angular-redux package that we use to interact with the redux store.

A sample implemention of the StateAwareResolver looks like -



const isStateValid = (user: User, route: ActivatedRouteSnapshot) => {
  return user !== undefined && user !== null;
};

const userStateSelector = (state: IAppState) => {
  return state.user;
};

class CurrentUserResolver extends StateAwareResolver<User> {
  constructor(
    redux: NgRedux<IAppState>,
    private userService: UserService,
    private userState: UserStateService
  ) {
    super(userStateSelector, ngRedux, isStateValid);
  }

  protected load(
    params: Params,
    queryParams: Params,
    state: IAppState
  ): Observable<User> {
    // loads the current user
    return this.userService.getCurrent();
  }

  protected setInState(value: User) {
    // set the user in state
    this.userState.setCurrent(value);
  }
}


In the route config -


// app.routes.ts
[
  {
    path: "/dashboard",
    component: DashboardComponent,
    resolve: {
      user: CurrentUserResolver,
      permissions: CurrentUserPermissionResolver,
    },
  },
  {
    path: "/user",
    component: UserComponent,
    resolve: {
      user: CurrentUserResolver,
    },
  },
]

In this case, when the user first navigates to the dashboard, the resolver will load the user over HTTP and set it in state, however, when the user navigates to /user, then the component will load immediately as the user is already present in state.

Using this little abstraction, we are not only making it easy to handle our state data dependencies, but also making our API consumption more efficient as any valid data will not be reloaded unnecessarily.