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:
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:
- Prioritize the models (or modules) in your Angular application that you want to upgrade to use NgRx.
- 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 aStateModule
that will be imported into theAppModule
. If you’re refactoring a model that is only used in a single module, perhaps theorder
model, then it’s possible that we can implement the actions, effects and reducers within a lazy-loaded module. - 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 persistinguser
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:
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
andStoreDevtoolsModule
modules that are provided by NgRx. - We then create the root store by invoking the
forRoot()
static method onStoreModule
. We are configuring therouterReducer
, 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 theStoreRouterConnectingModule
. - 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 theAppModule
via theconstrutor()
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:
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 theAppState
. - 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
andstoreFreeze
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
.