Feature Reducer With Ngrx Angular

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.

Feature Module

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/.

Feature Store

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.

Feature Effect

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.

Conclusion

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!

External Sources

Designed, built and maintained by Kimserey Lam.