Nov 1st, 2019 - written by Kimserey with .
Just like how Angular components, ngrx
stores can also be separated into different modules. This has the benefits of reducing the complexity of a system by having dedicated modules with dedicated reducers, actions and effects. Today we will see how to define ngrx
reducers, effects and actions for feature modules.
Let’s start by defining an example of an application with a feature module. We will create a User
module:
1
2
3
4
5
6
7
8
9
src/
app/
user/
user-routing.module.ts
user.component.ts
user.module.ts
app-rounting.module.ts
app.component.ts
app.module.ts
In order our routing module is empty for the moment:
1
2
3
4
5
6
7
8
9
10
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Then we have the most simplistic AppComponent
:
1
2
3
4
5
6
7
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>'
})
export class AppComponent { }
And we definbe the AppModule
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserModule } from './user/user.module';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
UserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Notice that we import UserModule
which we will define below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UserRoutingModule } from './user-routing.module';
import { UserComponent } from './user.component';
@NgModule({
declarations: [
UserComponent
],
imports: [
CommonModule,
UserRoutingModule
],
providers: []
})
export class UserModule { }
The UserModule
under /app/user/
has a rounting
module and a single component UserComponent
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserComponent } from './user.component';
const routes: Routes = [
{
path: 'user',
component: UserComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class UserRoutingModule { }
It provides a single route to /user
which displays the following component:
1
2
3
4
5
6
7
8
9
10
11
import { Component, Input } from '@angular/core';
@Component({
template: "<div></div>"
})
export class UserComponent {
username;
constructor() {
this.username = 'Kimserey';
}
}
From here we then have a complete app that will display Kimserey
when navigating to localhost:4200/user
.
We now have an application with a single feature module user
and we can then introduce ngrx
/.
To use ngrx store
, we first have to add the framework to our application:
1
npm install @ngrx/store --save
We also install the development tools:
1
npm install @ngrx/store-devtools --save
We can then create a state managing the username
in new folder username/
under user/
, where we add the reducer and actions.
1
2
3
4
5
app/
user/
username/
username.actions.ts
username.reducer.ts
We create two actions, the first one to replace the username currently displayed and a second action to reset it back.
1
2
3
4
5
6
7
8
9
// username/username.actions.ts
import { createAction, props } from '@ngrx/store';
export const update = createAction(
'[User - Username] Update',
props<{ name: string }>()
);
export const reset = createAction('[User - Username] Reset');
createAtion
is used to create ngrx
actions typesafed with props
. props<{ name: string }>()
provides typesafety by enforcing the type of the action payload, subsequently allowing us to get a typed paylod on the reducers and effects as we will see next. We then define the reducer file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// username/username.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';
import { update, reset } from './username.actions';
export const featureKey = 'username';
export interface State {
name: string;
}
export const initialState: State = {
name: 'Kimserey'
};
const _reducer = createReducer(initialState,
on(update, (_, props) => ({ name: props.name })),
on(reset, () => initialState)
);
export function reducer(state: State, action: Action) {
return _reducer(state, action);
}
export const getUsername = (state: State) => state.name;
The reducer handles the actions and update the state, a simple Javascript object containing the information about the username. We start by defining the State
as an interface, then create an initial value for it and we then create the reducer with createReducer
combined with on
. As we saw earlier, we defined the props
and we can see how it helped us in on(update, (_, props) => ({ name: props.name }))
where props
has a .name
property.
We define the reducer as a private variable and expose a function reducer
for AOT purposes.
Username
is one of the reducer of our User
feature, we can imagine how we can potentially have many reducers therefore we combine all our reducers into an ActionReducerMap<UserState>
where the UserState
is an interface containing all subreducers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// user.reducer.ts
import { InjectionToken } from '@angular/core';
import { ActionReducerMap } from '@ngrx/store';
import * as fromUsername from './username/username.reducer';
export const userFeatureKey = 'user';
export interface UserState {
[fromUsername.featureKey]: fromUsername.State;
}
export const reducers = new InjectionToken<ActionReducerMap<UserState>>(userFeatureKey, {
factory: () => ({
[fromUsername.featureKey]: fromUsername.reducer
})
});
export interface State extends fromRoot.State {
[userFeatureKey]: UserState;
}
const getUserFeatureState = createFeatureSelector<State, fromUsername.State>(userFeatureKey);
const getUsernameState = createSelector(getUserFeatureState, state => state[fromUsername.featureKey]);
export const getUsername = createSelector(getUsernameState, fromUsername.getUsername);
We create the ActionReducerMap<UserState>
within an InjectToken
for AOT purposes, and we define selectors to select the User
feature out of the main root state, and then create selectors to select the substate and then lastly a getUsername
selector to select the username out of the Username
state. We defined the State
extending the root state so that we can have a typesafe notation of with the selectors.
Then we can register the feature store in our user.module
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// user.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import * as fromUser from './user.reducer';
import { UserRoutingModule } from './user-routing.module';
import { UserComponent } from './user.component';
@NgModule({
declarations: [
UserComponent
],
imports: [
CommonModule,
UserRoutingModule,
StoreModule.forFeature(fromUser.userFeatureKey, fromUser.reducers)
],
providers: []
})
export class UserModule { }
StoreModule.ForFeature
can take either an ActionReducerMap
or an ActionReducer
, therefore if we had only one reducer in the feature, we could have registered it here.
Throughout the whole definition of the username
reducer and user
reducer, we have been using feature keys
to register each reducer. The feature key will define the name of the property in the state therefore here the final state will have the following form:
1
2
3
4
5
6
7
{
"user": {
"username": {
"name": "Kimserey"
}
}
}
"user"
is the state at the feature level, and "username"
is the state at the subfeature level. Had we register directly the username
reducer, we would have had a single level:
1
2
3
4
5
{
"username": {
"name": "Kimserey"
}
}
Lastly we can register the store in the AppModule
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserModule } from 'src/user/user.module';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
UserModule,
StoreModule.forRoot({}),
StoreDevtoolsModule.instrument()
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We specify empty object {}
as we don’t have reducers on the main app module, and we import UserModule
which will have as effect to load the feature module in the state.
Effects definition are roughly the same as reducers. We start by installing the effect library:
1
npm install @ngrx/effects --save
And we register effects in feature module with:
1
EffectsModule.forFeature([UserEffects])
And we register effects on the root application with:
1
EffectsModule.forRoot([])
Just like StoreModule.forRoot
, it is imperative to register the root effects and store modules even if we don’t have reducers and effects in the root of the application.
Today we continued our journey with Angular modules and saw how we could define feature modules which includes feature stores and feature effects with ngrx
. We started by creating the most simplistic example of an application with a single feature module, then we moved on to create a feature store and most importantly we saw how to register feature store in an AOT friendly way. Lastly we completed the post by looking briefly at how we could register feature effects. I hope you liked this post and I see you on the next one!