Brian Love
Angular + TypeScript Developer in Denver, CO

Angular + NgRx: Getting Started

Reading time ~8 minutes

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:

  • Angular 5.0.2
  • RxJS 5.5.2
  • TypeScript 2.4.2

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:

  • First, we have imported the StoreModule, StoreRouterConnectingModule and StoreDevtoolsModule modules that are provided by NgRx.
  • We then create the root store by invoking the forRoot() static method on StoreModule. We are configuring the routerReducer, which connects the Angular router with the NgRx store.
  • The ordering of the imports is import. We must import the StoreModule first.
  • We then invoke the forRoot() method on the StoreRouterConnectingModule.
  • We are also going to configure the DevTool instrumentation so we can use the DevTools chrome extension by invoking the StoreDevtoolModule.instrument() static method. Note that we only use the DevTools when our application is not executing in the production environment.
  • Finally, we prevent the StateModule from being imported in any module other than the AppModule via the construtor() function.

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:

  • See actions that are dispatched to the store.
  • View the entire state tree of our application after each action is dispatched.
  • Manually dispatch actions to the store.
  • Scrub through the history of the state of our application.

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:

  • Right now the only object that we have defined within our state tree is the router reducer, which is stored in the router property in the AppState.
  • I’ve created a meta reducer function called logger(). While this is not necessary for your application, and the Chrome DevTools provides more than adequate insight into the actions that are dispatched, and the mutatations to the state of our application over time, this is a good example of a meta reducer and how to implement one.
  • Note tht we only use the logger and storeFreeze meta reducers when our application is not in a production environment.

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.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).