Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Angular + NgRx: Getting Started

Get started with NgRx in your Angular applications.

Series

This post is part of a series on using NgRX, the Redux-inspired, predictable state container for JavaScript:

  1. NgRX: The Basics
  2. NgRX: Getting Started (this post)
  3. NgRX: Refactor Module

Starting from Scratch

If you’re starting from scratch with a new Angular project I recommend that you first set yarn as the global package manager for the Angular CLI:

ng set --global packageManager=yarn

Then, create a new Angular application, we’ll call it client:

$ ng new client

Existing Project

If you have an existing project and want to start implementing the Redux pattern using NgRx in your Angular application then you should make a plan to:

  1. Prioritize the models (or modules) in your Angular application that you want to upgrade to use NgRx.
  2. Determine if the model is cross-cutting. If you’re going to refactor the user model, then it’s likely used throughout your application, and we will implement the actions, effects and reducers in a StateModule that will be imported into the AppModule. If you’re refactoring a model that is only used in a single module, perhaps the order model, then it’s possible that we can implement the actions, effects and reducers within a lazy-loaded module.
  3. Commit to upgrading all implementations of the chosen model within the release. You may see some odd behavior when parts of your application are selecting user objects from the store, and other parts of your application are relying on the service or another class for obtaining, mutating or persisting user objects.

It’s important to note that you don’t need to refactor the entire application to use the Redux pattern implemented by NgRx, you can take it module-by-module and tackle each one separately.

Example Project

In this post I’ll be working with an example application that I wrote on New Year’s eve 2017. I spent about 6 hours developing a Tour of Heroes application that uses:

I’m also using Angular’s Material UI and Flex Layout modules. You can download the source code and follow along or fork the repository on GitHub:

Note that if you fork the repository, I’ll be working from the start branch.

The example project uses json-server to provide a simple REST API that our client Angular application can consume.

To install and start the server:

$ cd server
$ yarn install
$ yarn serve

The server is now available at: http://localhost:3000.

We also have static files being served out of the server/static directory. The example application uses a few images that are stored in server/static/img.

To install and start the client:

$ cd client
$ yarn install
$ ng serve

Now, navigate to http://localhost:4200 and you should see:

Tour of Heroes Application

The Basics

If you’re new to NgRx, check out my post on the basics of the Redux pattern implemented by NgRx. I go over the pros and cons of using Redux, and the core concepts.

Installation

The first step is to install @ngrx modules. We’ll also be installing the ngrx-store-freeze module to prevent any possible attempts to mutate the objects in the store.

$ yarn add @ngrx/store @ngrx/router-store @ngrx/effects @ngrx/store-devtools @ngrx/entity ngrx-store-freeze

StateModule

We’ll be implementing cross-cutting concerns in a StateModule that will be imported in the AppModule. The actions, effects, and reducers that we implement in the StateModule will be available throughout all of the modules in our application. In fact, they’ll be shipped with the initial bundle file that is compiled by the Angular application running in the user’s browser.

Let’s start by creating the module using the CLI:

$ ng g m state

This will result in a new src/app/state directory, which contains the src/app/state/state.module.ts file. Let’s go ahead and update the file:

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot({ routerReducer: routerReducer}),
    StoreRouterConnectingModule.forRoot(),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: []
})
export class StateModule {

  constructor(@Optional() @SkipSelf() parentModule: StateModule) {
    if (parentModule) {
      throw new Error(
        'StateModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(): ModuleWithProviders {
    return {
      ngModule: StateModule,
      providers: []
    };
  }

}

A few things to note:

We can then invoke the StateModule.forRoot() static method in our AppModule:

import { StateModule } from './state/state.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    AppRoutingModule,
    BrowserModule,
    BrowserAnimationsModule,
    CoreModule.forRoot(),
    SharedModule,
    StateModule.forRoot(),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

The only line that was added to the AppModule is the last import: StateModule.forRoot(). If we view the application in our browser, nothing has changed, and we should not have any exceptions in the console.

Redux DevTools

Before we go any further, I should note that one of the benefits of using Redux is the developer debugging experience. One of the most useful tools when using Redux is the Redux DevTools Chrome extension.

We’ll be able to use the DevTools to:

After you install the DevTools, open up the Redux tab in Chrome’s development tools and you should see the ROUTER_NAVIGATION action being dispatched:

Chrome Redux DevTools

AppState Interface

The next step in getting started with NgRx is defining the AppState interface. Create a new file at src/app/state/app.interface.ts:

import { RouterReducerState } from '@ngrx/router-store';
import { RouterStateUrl } from './shared/utils';

export interface AppState {
  router: RouterReducerState<RouterStateUrl>;
}

Note that I’m referencing a utils.ts file. We haven’t created that yet, but we will shortly.

AppEffects Class

Next, we’ll create a root AppEffects class. We can put any effects in this class that we want to do at the root level of our application, perhaps some sort of error notification could be added here. For now, let’s just stub it out.

Create a new file src/app/state/app.effects.ts:

import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';

@Injectable()
export class AppEffects {

  constructor(private actions: Actions) { }

}

Then, update the StateModule to import the EffectsModule from NgRx and wire up the AppEffects class.

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot({ routerReducer: routerReducer }),
    StoreRouterConnectingModule.forRoot(),
    EffectsModule.forRoot([AppEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  providers: [PowersService],
})
export class StateModule {
  // code omitted
}

Note that the only change above is importing the EffectsModule.forRoot() where we provide an array of classes, with just the single AppEffects class.

appReducer() Function

We’re almost there with getting started with NgRx. Our next task is to create the root appReducer function.

Create a new file at src/app/state/app.reducer.ts:

import { routerReducer } from "@ngrx/router-store";
import { ActionReducer, ActionReducerMap, MetaReducer } from "@ngrx/store";
import { storeFreeze } from "ngrx-store-freeze";
import { environment } from "../../environments/environment";
import { AppState } from "./app.interfaces";

export const appReducer: ActionReducerMap<AppState> = {
  router: routerReducer
};

export function logger(reducer: ActionReducer<AppState>): ActionReducer<AppState> {
  return function(state: AppState, action: any): AppState {
    console.log("state", state);
    console.log("action", action);
    return reducer(state, action);
  };
}

export const appMetaReducers: MetaReducer<AppState>[] = !environment.production
  ? [logger, storeFreeze]
  : [];

A few things to note:

Shared Utils

Finally, we are going to use some shared utils. Create a new src/app/state/shared/utils.ts file:

import { Params, RouterStateSnapshot } from '@angular/router';
import { RouterStateSerializer } from '@ngrx/router-store';

const typeCache: { [label: string]: boolean } = {};

export function createActionType<T>(label: T | ''): T {
  if (typeCache[<string>label]) {
    throw new Error(`Action type "${label}" is not unique"`);
  }

  typeCache[<string>label] = true;

  return <T>label;
}

export interface RouterStateUrl {
  url: string;
  params: Params;
  queryParams: Params;
}

export class CustomSerializer implements RouterStateSerializer<RouterStateUrl> {
  serialize(routerState: RouterStateSnapshot): RouterStateUrl {
    let route = routerState.root;
    while (route.firstChild) {
      route = route.firstChild;
    }

    const { url } = routerState;
    const queryParams = routerState.root.queryParams;
    const params = route.params;

    // Only return an object including the URL, params and query params
    // instead of the entire snapshot
    return { url, params, queryParams };
  }
}

The last step is to refactor our StateModule slightly. We are going to wire up our root appReducer() function as well as our appMetaReducers function. We’ll also need to provide a RouterStateSerializer.

Here is the full StateModule source code:

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot(appReducer, {
      metaReducers: appMetaReducers
    }),
    StoreRouterConnectingModule.forRoot(),
    EffectsModule.forRoot([
      AppEffects
    ]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: []
})
export class StateModule {

  constructor(@Optional() @SkipSelf() parentModule: StateModule) {
    if (parentModule) {
      throw new Error(
        'StateModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(): ModuleWithProviders {
    return {
      ngModule: StateModule,
      providers: [
        {
          provide: RouterStateSerializer,
          useClass: CustomSerializer
        }
      ]
    };
  }
}

Conclusion

In this post we’ve configured a root AppState that has a single router property, which uses the @ngrx/router-store module to bind the Angular router to the NgRx store. We haven’t yet refactored any of the existing appliation code to implement NgRx.

The next step for the application is to define actions, effects and reducers for the Power model. Then, we’ll refactor the PowerModule to use the Store, dispatching actions to LoadPowers, AddPower and UpdatePower.