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

Brian Love

Updating To NgRx 4

Here’s a quick guide to updating your code to use NgRx 4.

In case you missed it, NgRx 4 dropped on July 18. I’m going to review some of the changes that I had to implement in my codebase to update to this latest release.

TypeScript v2.4+

First, you need to ensure that you are using version 2.4+ of TypeScript. Update your project’s package.json file to:

{
  "devDependencies": {
    "typescript": "^2.5.2"
  }
}

Make sure you remove your project’s TypeScript installation, and then run npm install:

$ rm -rf ./node_modules/typescript
$ npm install

RxJS v5.4+

Next, ensure that you have installed the latest version of RxJS. Update your project’s package.json file to:

{
  "dependencies": {
    "rxjs": "^5.4.3"
  }
}

Again, make sure you remove your project’s RxJS installation, and then run npm install:

$ rm -rf ./node_modules/rxjs
$ npm install

Actions

The way you define your actions using NgRx v4 is not that much different that how you defined actions using v2/3. Let’s quickly review how we define actions:

// @ngrx
import { Action } from "@ngrx/store";

// models
import { Activity } from "../../models/activity";

// activities
export const LOAD_ACTIVITY = "[activities] Load activity";
export const LOAD_ACTIVITY_ERROR = "[activities] Load activity error";
export const LOAD_ACTIVITY_SUCCESS = "[activities] Load activity success";

/**
 * Load activity.
 * @class LoadActivityAction
 * @implements {Action}
 */
export class LoadActivityAction implements Action {
  readonly type = LOAD_ACTIVITY;
  constructor(public payload: { id: string }) {}
}

/**
 * Load activity error.
 * @class LoadActivityErrorAction
 * @implements {Action}
 */
export class LoadActivityErrorAction implements Action {
  readonly type = LOAD_ACTIVITY_ERROR;
  constructor(public payload: { error: Error }) {}
}

/**
 * Load activity success.
 * @class LoadActivitySuccessAction
 * @implements {Action}
 */
export class LoadActivitySuccessAction implements Action {
  readonly type = LOAD_ACTIVITY_SUCCESS;
  constructor(public payload: { activity: Activity }) {}
}

/**
 * Actions type.
 * @type {Actions}
 */
export type Actions
  = LoadActivityAction
  | LoadActivityErrorAction
  | LoadActivitySuccessAction;

Above is an example of three actions that load an activity (from a REST API). Along with the LoadActivityAction, I also define LoadActivityErrorAction and LoadActivitySuccessAction actions for when an exception occurs or when the activity is successfully loaded.

A few things to note:

This should look very familiar to your NgRx v2/3 code, as nothing has changed.

Next, let’s look at the changes to how we navigate using NgRx v4. The first thing you will notice is that the navigation functions are gone. This includes: go(), back() and forward() functions defined in the @ngrx/router-store module.

So, what do we do?

Simple. We will create our own actions.

First, create a new src/app/actions/router.ts file in your application:

$ cd src/app
$ mkdir actions
$ touch router.ts

Here is the full contents of my router.ts file:

// ngrx
import { Action } from "@ngrx/store";
import { NavigationExtras } from "@angular/router";

export const GO = "[Router] Go";
export const BACK = "[Router] Back";
export const FORWARD = "[Router] Forward";

export class Go implements Action {
  readonly type = GO;

  constructor(public payload: {
    path: any[];
    query?: object;
    extras?: NavigationExtras;
  }) {}
}

export class Back implements Action {
  readonly type = BACK;
}

export class Forward implements Action {
  readonly type = FORWARD;
}

export type Actions
  = Go
  | Back
  | Forward;

This should look very familiar to the actions that we defined previously. In this case, we are defining three new action classes: Go, Back and Forward.

Note that the Go class’s payload has three properties defined:

Note that the query and extras properties are optional.

Next, we need to define side effects for each of these navigation classes. To do so, create a new src/app/effects/router.ts file:

$ cd src/app
$ mkdir effects
$ touch router.ts

Here is the full contents of the src/app/effects/router.ts file:

import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { Location } from "@angular/common";
import { Effect, Actions } from "@ngrx/effects";
import "rxjs/add/operator/do";
import "rxjs/add/operator/map";
import * as RouterActions from "../actions/router";

@Injectable()
export class RouterEffects {

  @Effect({ dispatch: false })
  navigate$ = this.actions$.ofType(RouterActions.GO)
    .map((action: RouterActions.Go) => action.payload)
    .do(({ path, query: queryParams, extras}) => this.router.navigate(path, { queryParams, ...extras }));

  @Effect({ dispatch: false })
  navigateBack$ = this.actions$.ofType(RouterActions.BACK)
    .do(() => this.location.back());

  @Effect({ dispatch: false })
  navigateForward$ = this.actions$.ofType(RouterActions.FORWARD)
    .do(() => this.location.forward());

  constructor(
    private actions$: Actions,
    private router: Router,
    private location: Location
  ) {}
}

A few things to note:

Ok, so now let’s look at the implementation. Here is what our navigation implementation was previously:

import { go } from "@ngrx/router-store";

public editActivity(activity: Activity) {
  this.store.dispatch(go(["/activities/edit", activity._id]));
}

Here is the refactored navigation using the new Go action:

import { Go } from "../actions/router";

public editActivity(activity: Activity) {
  this.store.dispatch(new Go({
    path: ["/activities/edit", activity._id]
  }));
}

A few things to note:

Effects

The effects API for registering effects has been updated. Previously you had to invoke the run() function for each effects class in your application. This has been deprecated in favor of two new functions: forRoot() and forFeature(). The forRoot() function is invoked in your root AppModule, and forFeature() is invoked in any subsequent feature modules.

For example, before my application’s AppModule class decorator was:

import { EffectsModule } from '@ngrx/effects';

@NgModule({
  // code omitted
  imports: [EffectsModule.run(ActivityEffects), EffectsModule.run(UserEffects)],
})
export class AppModule {}

Now, it’s been updated to use the forRoot() function:

import { EffectsModule } from '@ngrx/effects';

import { RouterEffects } from './effects/router';

@NgModule({
  // code omitted
  imports: [EffectsModule.forRoot([RouterEffects])],
})
export class AppModule {}

Note that the only effects class that I am configuring in my AppModule is the RouterEffects. While previously I had configured all of the effects classes for my entire app. Your application may be structured different than mine, but I think this is worth noting. I am now wiring up my effects in each lazy-loaded module as appropriate, rather than in the root AppModule.

Typed Payload

The @ngrx/effects module included a helper toPayload() function that was used in conjuction with the map() function to return the payload for the current action. While this was helpful, the issue was that the function returns any.

Here was the signature for the function:

export declare function toPayload(action: Action): any;

This is an example of how it was used:

@Effect()
public delete: Observable<Action> = this.actions
  .ofType(DELETE_ACTIVITY)
  .map(toPayload)
  .switchMap(payload => {
    return this.activityService.delete(payload.activity)
      .map(() => new DeleteActivitySuccessAction({ activity: payload.activity }))
      .catch(error => Observable.of(new DeleteActivityErrorAction({ error: error })));
  });

In the example above, note the use of the toPayload() function. This will return an observable with just the payload for the current action, in this case the DELETE_ACTIVITY action.

The issue is that the payload object that is passed to the switchMap() function above is of type any. This means we have no type safety, and we do not get any help in our editor about what properties the payload object has.

Here is an example of using a typed payload with NgRx v4:

@Effect()
public delete: Observable<Action> = this.actions
  .ofType(DELETE_ACTIVITY)
  .map((action: DeleteActivityAction) => action.payload)
  .switchMap(payload => {
    return this.activityService.delete(payload.activity)
      .map(() => new DeleteActivitySuccessAction({ activity: payload.activity }))
      .catch(error => Observable.of(new DeleteActivityErrorAction({ error: error })));
  });

We simply use a fat arrow function to specify the type of the action, in this case DeleteActivitySuccessAction, which returns the payload from the action. Now, the payload argument to the switchMap() method includes type safety.

Reducers

A lot changed in how you configure and combine your application’s reducers in NgRx v4. The good thing is that nothing will change in your pure reducer() functions.

I’m not sure if I was following best practices or not, but previously I had set up a single src/app/app.reducers.ts file for combining all of my reducers for my application:

import { createSelector } from "reselect";

// import @ngrx
import { ActionReducer, combineReducers } from "@ngrx/store";
import { compose } from "@ngrx/core/compose";
import { routerReducer, RouterState } from "@ngrx/router-store";
import { storeFreeze } from "ngrx-store-freeze";

// import environment
import { environment } from "../environments/environment";

/**
 * Every reducer module"s default export is the reducer function itself. In
 * addition, each module should export a type or interface that describes
 * the state of the reducer plus any selector functions. The `* as`
 * notation packages up all of the exports into a single object.
 */
import * as activities from "./activities/activities.reducers";
import * as users from "./users/users.reducers";

/**
 * We treat each reducer like a table in a database.
 * This means our top level state interface is just a map of keys to inner state types.
 */
export interface State {
  activities: activities.State;
  router: RouterState;
  users: users.State;
}

/**
 * Because metareducers take a reducer function and return a new reducer,
 * we can use our compose helper to chain them together. Here we are
 * using combineReducers to make our top level reducer, and then
 * wrapping that in storeLogger. Remember that compose applies
 * the result from right to left.
 */
const reducers = {
  activities: activities.reducer,
  router: routerReducer,
  users: users.reducer
};

// development reducer includes storeFreeze to prevent state from being mutated
const developmentReducer: ActionReducer<State> = compose(storeFreeze, combineReducers)(reducers);

// production reducer
const productionReducer: ActionReducer<State> = combineReducers(reducers);

/**
 * The single reducer function.
 * @function reducer
 * @param {any} state
 * @param {any} action
 */
export function reducer(state: any, action: any) {
  if (environment.production) {
    return productionReducer(state, action);
  } else {
    return developmentReducer(state, action);
  }
}

/**********************************************************
 * Activities Reducers
 *********************************************************/

/**
 * Returns the activities state
 */
export const getActivitiesState = (state: State) => state.activities;

/**
 * Returns the activities.
 */
export const getActivities = createSelector(getActivitiesState, activities.getActivities);

/**
 * Returns the activity.
 */
export const getActivity = createSelector(getActivitiesState, activities.getActivity);

/**********************************************************
 * Users Reducers
 *********************************************************/

/**
 * Returns the user state.
 */
export const getUsersState = (state: State) => state.users;

/**
 * Returns the authenticated user
 */
export const getAuthenticatedUser = createSelector(getUsersState, users.getAuthenticatedUser);

/**
 * Returns the authentication error.
 */
export const getAuthenticationError = createSelector(getUsersState, users.getAuthenticationError);

/**
 * Returns true if the user is authenticated
 */
export const isAuthenticated = createSelector(getUsersState, users.isAuthenticated);

Wow, that’s long! And the worst part is, that is just a small part of my application’s reducer functions. I would guess there are over 100 functions that I had previously defined in this reducers file.

The first thing I did was to delete this file.

Then, following the example application architecture I started by creating a reducers directory in each module.

Let’s focus on my src/app/activities/reducers directory. In here, I have three files:

  1. src/app/activities/reducers/activities.ts
  2. src/app/activities/reducers/index.ts
  3. src/app/activities/reducers/modules.ts

In this modules I have multiple state objects: activities and modules. This takes advantage of NgRx’s fractal state management:

Store uses fractal state management, which provides state composition through feature modules, loaded eagerly or lazily.

Let’s look at the ActivitiesState and reducer in src/app/activities/reducers/activities.ts:

// import actions
import {
  LOAD_ACTIVITY,
  LOAD_ACTIVITY_ERROR,
  LOAD_ACTIVITY_SUCCESS,
  Actions
} from "../actions/activity";

// import models
import { Activity } from "../../models/activity";

/**
 * The state.
 */
export interface State {
  activity?: Activity;
  error?: Error;
  loading: boolean;
}

/**
 * The initial state.
 */
const initialState: State = {
  loading: false,
};

/**
 * The reducer function.
 */
export function reducer(state: State = initialState, action: Actions): State {
  switch (action.type) {
    case LOAD_ACTIVITY:
      return { ...state, ...{
        activity: undefined,
        error: undefined,
        loading: true
      }};

    case LOAD_ACTIVITY_ERROR:
      return { ...state, ...{
        error: action.payload.error,
        loading: false
      }};

    case LOAD_ACTIVITY_SUCCESS:
      return { ...state, ...{
        activity: action.payload.activity,
        loading: false
      }};

    default:
      return state;
  }
}

/**
 * Return true if the activity is loading
 */
export const isLoading = (state: State) => state.loading;

/**
 * Return the activity
 */
export const getActivity = (state: State) => state.activity;

A few things to note here:

This should look very familiar, as this is exactly what we did using NgRx v2/3.

Now, for clarity, let’s look at the src/app/activities/reducers/modules.ts file:

// import actions
import {
  LOAD_ACTIVITIES_FOR_MODULE,
  LOAD_ACTIVITIES_FOR_MODULE_ERROR,
  LOAD_ACTIVITIES_FOR_MODULE_SUCCESS,
  Actions
} from "../actions/module";

// import models
import { Activity } from "../../models/activity";
import { Module } from "../../models/module";

/**
 * The state.
 */
export interface State {
  activities?: Activity[];
  error?: Error;
  loading: boolean;
  module?: Module;
}

/**
 * The initial state.
 */
const initialState: State = {
  activities: [],
  loading: false
};

/**
 * The reducer function.
 */
export function reducer(state: State = initialState, action: Actions): State {
  switch (action.type) {
    case LOAD_ACTIVITIES_FOR_MODULE:
      return { ...state, ...{
        activities: [],
        error: undefined,
        module: action.payload.module
      }};

    case LOAD_ACTIVITIES_FOR_MODULE_ERROR:
      return { ...state, ...{
        error: action.payload.error,
        loading: false
      }};

    case LOAD_ACTIVITIES_FOR_MODULE_SUCCESS:
      return { ...state, ...{
        activities: action.payload.activities,
        loading: false
      }};

    default:
      return state;
  }
}

/**
 * Return true if the activities are loading
 */
export const areActivitiesLoading = (state: State) => state.loading;

/**
 * Return activities.
 */
export const getActivities = (state: State) => state.activities;

This not very different from the state management for our activities. In this case, I am managing the state of activities for a specific module. While, this is specific to my application, I think this will be helpful to show how we compose multiple state managements

Now, let’s look at our state composition in the src/app/activities/reducers/index.ts file:

// ngrx
import { createSelector, createFeatureSelector } from "@ngrx/store";

// reducers
import { State as RootState } from "../../reducers";
import * as fromActivities from "./activities";
import * as fromModules from "./modules";

export interface ActivitiesState {
  activities: fromActivities.State;
  modules: fromModules.State;
}

export interface State extends RootState {
  "activities": ActivitiesState;
}

export const reducers = {
  activities: fromActivities.reducer,
  modules: fromModules.reducer
};

/**
 * The createFeatureSelector function selects a piece of state from the root of the state object.
 * This is used for selecting feature states that are loaded eagerly or lazily.
*/
export const getActivitiesState = createFeatureSelector<ActivitiesState>("activities");

/**
 * Every reducer module exports selector functions, however child reducers
 * have no knowledge of the overall state tree. To make them useable, we
 * need to make new selectors that wrap them.
 *
 * The createSelector function creates very efficient selectors that are memoized and
 * only recompute when arguments change. The created selectors can also be composed
 * together to select different pieces of state.
 */
export const getActivityEntityState = createSelector(
  getActivitiesState,
  (state: ActivitiesState) => state.activities
);

export const getModulesEntityState = createSelector(
  getActivitiesState,
  (state: ActivitiesState) => state.modules
);

/**
 * Returns the activity.
 */
export const getActivity = createSelector(getActivityEntityState, fromActivities.getActivity);

/**
 * Returns the module activities.
 */
export const getModuleActivities = createSelector(getModulesEntityState, fromModules.getActivities);

Let’s review our state composition:

I should also make sure to mention that we are no longer dependent on the createSelector() method in the reselect module. You can safely remove this dependency from your package.json file after upgrading to NgRx v4.

Now, let’s finish upgrading our reducers by defining the root State interface. We’ll be using the @ngrx/router-store to bind Angular’s router to our store.

First, I created a new src/app/reducers/index.ts file:

$ cd src/app
$ mkdir reducers
$ touch index.ts

Here is the full contents of the src/app/reducers/index.ts file:

// import @ngrx
import {
  ActionReducer,
  ActionReducerMap,
  createSelector,
  createFeatureSelector,
  MetaReducer
} from "@ngrx/store";
import { RouterReducerState, routerReducer } from "@ngrx/router-store";
import { RouterStateUrl } from "../shared/utils";

// import environment
import { environment } from "../../environments/environment";

/**
 * As mentioned, we treat each reducer like a table in a database. This means
 * our top level state interface is just a map of keys to inner state types.
 */
export interface State {
  routerReducer: RouterReducerState<RouterStateUrl>;
}

/**
 * Our state is composed of a map of action reducer functions.
 * These reducer functions are called with each dispatched action
 * and the current or initial state and return a new immutable state.
 */
export const reducers: ActionReducerMap<State> = {
  routerReducer: routerReducer
};


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

/**
 * By default, @ngrx/store uses combineReducers with the reducer map to compose
 * the root meta-reducer. To add more meta-reducers, provide an array of meta-reducers
 * that will be composed to form the root meta-reducer.
 */
export const metaReducers: MetaReducer<State>[] = !environment.production
? [logger]
: [];

I can’t take any credit for this, as I took this straight from the example application provided with NgRx v4.

Now, let’s go back to our AppModule and look at our necessary changes. Before, we wired up the router and store as follows:

import { RouterStoreModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';

import { reducer } from './app.reducers';

@NgModule({
  // code omitted
  imports: [
    RouterStoreModule.connectRouter(),
    StoreModule.provideStore(reducer, {
      router: window.location.pathname + window.location.search,
    }),
  ],
})
export class AppModule {}

After our upgrade to NgRx v4 our AppModule should look like:

import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';

import { metaReducers, reducers } from './reducers';

@NgModule({
  // code omitted
  imports: [
    StoreRouterConnectingModule,
    StoreModule.forRoot(reducers, { metaReducers }),
  ],
})
export class AppModule {}

A few things to note:

Note that we use the forRoot() method in this instance because this is our root AppModule. Let’s quickly review how we use the forFeature() method in a feature module.

Here is a snippet from the ActivitiesModule in src/app/activities/activities.module.ts:

import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';

import { ActivityEffects } from './effects';
import { reducers } from './reducers';

@NgModule({
  // code omitted

  imports: [
    EffectsModule.forFeature([ActivityEffects]),
    StoreModule.forFeature('activities', reducers),
  ],
})
export class ActivitiesModule {}

A few things to note:

Conclusion

In conclusion, I highly recommend you check out some of the resources provided by the NgRx team: