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:
- First, we import the
Action
interface from @ngx/store. - Next, we define constant string values for each action.
- Then, we define an action class that implements the
Action
interface for each action. The publictype
parameter is read-only, and is set to the value of the constant string. We also define theconstructor()
function for each class, which contains a publicly availablepayload
property. Each payload is anObject
with the necessary properties for the action.
This should look very familiar to your NgRx v2/3 code, as nothing has changed.
Navigation
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:
path
: specify an array of commands that match Angular’s Router.navigate() method.query
: specify an object of name/value pairs for query params, similar to Angular’s Params typeextras
: specify an object ofNavigationExtras
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:
- First, the
RouterEffects
class is decorated with the@Injectable()
decorator. - Next, we define a property for each navigation action. Each property is decorated with the
@Effect()
decorator. We set thedispatch
property value tofalse
in the decorator to note that the effect will not dispatch any new actions. - Within each property we invoke the appropriate method on either the
Router
or on theLocation
instance that is injected via theconstructor()
function. - We use the
do()
RxJs method to transparently perform a side effect. You can learn more about thedo()
utility method at learnrxjs.io. - Also, note the use of the
map()
operator to extract thepayload
object that is provided with theGo
class’s constructor function.
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:
- First, we no longer import the
go()
function from the @ngrx/router-store module. Instead, we import theGo
class from the router module we previously defined. - We still use the
Store.dispath()
method to dispatch the action. - We dispatch a
new
instance of theGo
class, specifying the payload object. In this example, I am specifying only thepath
property. Note that the value of thepath
is the same as before.
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:
- src/app/activities/reducers/activities.ts
- src/app/activities/reducers/index.ts
- 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:
- First, we import the action constant strings along with the
Actions
type for this feature. - Next, we define the
State
interface. - Then, we define our
initialState
object that implements theState
interface. - Then, we define the pure
reducer()
function that modifies our state based on the dispatched action to the store. - Finally, we define state selector functions that return the value of the
loading
boolean value, and theactivity
that has been loaded.
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:
- First, we import the
createSelector()
andcreateFeatureSelector()
functions from the @ngrx/store module. - Next, we import our root
State
interface that is defined in src/app/reducers/index.ts. We will take a look at this next. - Then, we import each fractal state management reducer. In this case I am importing all exported members from each module: src/app/activities/reducers/activities.ts and src/app/activities/reducers/modules.ts.
- Now we can define our composed
ActivititeState
interface. I am calling it this because this is for theActivitiesModule
module in my application, which is lazy-loaded. - Next, we define a
State
interface that extends theRootState
, specifying the feature state. - Then, we define a
reducers
object that contains both reducer functions for this module. We defined these previously. - Then, using the
createFeatureSelector()
convenience method we create the feature state. - Finally, we define selectors using
createSelector()
convenience method. Note that we first specify the appropriate feature selector function, and then we specify the selector functions that we defined in each module.
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:
- First, we import the new
StoreRouterConnectingModule
module from @ngrx/router-store. TheRouterStoreModule
module has been deprecated in NgRx v4. - Second, we no longer import the single
reducer
function. If you recall from my v2/3 code (and from the v2/3 example app) we used either thecompose()
function when in development, or thecombineReducers()
function when in production to create a single reducer function. This was passed into theStoreModule.provideStore
method, which has also been deprecated in v4. - Next, we import the
metaReducers
andreducers
from our new src/app/reducers/index.ts module. - Then, we specify the
StoreRouterConnectingModule
module in theimports
array. Note that we just import the module. We do not need to invoke theconnectRouter()
method like we did previously; again, this as been deprecated in v4. - Finally, we invoke the
forRoot()
method on theStoreModule
class, specifying ourreducers
.
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:
- First, we import the
EffectsModule
andStoreModule
from @ngrx/effects and @ngrx/store, respectively. - Then, we import the
ActivitiesEffects
class. These are the side effects that we defined for this feature module. - Then, we import the
reducers
object that contains each state’s reducer function. - Then, we invoke the
EffectsModule.forFeature()
method, providing an array of effects for this feature. - Finally, we invoke the
StoreModule.forFeature()
method, specifying the name of the feature state, and then the reducers for the state.
Conclusion
In conclusion, I highly recommend you check out some of the resources provided by the NgRx team: