Real World App - Part 18: Revisiting ngrx

ngrx 5 - splitting store into feature modules, using action classes, selectors & router-store

This is Part 18 of our Real World App series. For other parts, see links at the bottom of this post or go to https://blog.realworldfullstack.io/. The app is currently under development and can be viewed at https://rwa-trivia.firebaseapp.com/ and the github repo can be found here.

Recap

In Part 4 of the series, I explained how we managed state of our application using ngrx. ngrx has undergone quite a few changes since and we’ll upgrade our application to reflect on those changes.

In this part we will upgrade our app to improve our state management using ngrx 5 and the new features it provides


In upgrading our application store to ngx 5 we will look at the following features -

  • Using selectors
  • Split up store into feature modules
  • Replace action functions with action classes
  • Using pipeable functions in effects
  • router-store
  • Router actions in effects
  • Use of entities (as needed)

Using selectors

Selectors are methods used for obtaining slices of store state.

A couple of links that explain it in depth -

I would start by refactoring categoryDictionary, making it a selector, instead of a reducer -

// categories.reducer.ts
...
export const getCategoryDictionary = (state: Category[]) => {
let categoryDict: {[key: number]: Category} = {};
state.forEach(category => {
categoryDict[category.id] = category;
});
return categoryDict;
}
...

and in app-store.ts

// app-store.ts
...
export const getState = (state: AppStore) => state;
//Categories selector
export const getCategories = createSelector(getState, (state: AppStore) => state.categories);
export const categoryDictionary = createSelector(getCategories, getCategoryDictionary);
...

We can then refer to the selector by simply using store.select(categoryDictionary)

Code for this commit.


Split up store into feature modules

ngrx 2 allowed us to have the state of our application in a central store, but didn’t have any easy mechanism to split that into feature modules.

This limitation meant we had to put our entire store in our root module, instead of placing it in relevant modules, even for lazy loaded features where at times we may never need those slices of the store to be loaded in our app. We start by moving the game-play part of the store in the game-play module.

I’ll not go into details of the code as most of it is simply moving the relevant store parts from core feature to game-play feature. A couple of things to note -

The game-play.module.ts will create a StoreModule for feature

// game-play.module.ts
...
  imports: [ ...
//ngrx feature store
StoreModule.forFeature('gameplay', reducer),
//ngrx effects
EffectsModule.forFeature(effects),
],
...

And we create a feature selector in app-store

//app-store.ts
...
export const gameplayState = createFeatureSelector<GamePlayState>('gameplay');
...

We can then refer to our gameplayState using ->

this.gameObs = store.select(gameplayState).select(s => s.currentGame)

Complete code for this commit.


Replace action functions with action classes

The payload property was removed from the Action interface as it was a source of type-safety issues. We had temporarily gotten around this by providing a ActionWithPayload interface in Part 14.

Here we completely replace these functions with classes and use the constructor to provide the payload.

//game-play.actions.ts
...
export enum GamePlayActionTypes {
RESET_NEW = '[GamePlay] ResetNew',
CREATE_NEW = '[GamePlay] CreateNew',
...
}
export class ResetNewGame implements Action {
readonly type = GamePlayActionTypes.RESET_NEW;
payload = null;
}
export class CreateNewGame implements Action {
readonly type = GamePlayActionTypes.CREATE_NEW;
constructor(public payload: {gameOptions: GameOptions, user: User}) {}
}
...
export type GamePlayActions
= ResetNewGame
| CreateNewGame
...

and we use it by creating a new instance -

this.store.dispatch(new gameplayactions.CreateNewGame({gameOptions: gameOptions, user: user}))

Complete code for the commit.


Using pipeable operators in effects

While pipeable operators are a new RxJs feature, rather than an ngrx feature, we use the opportunity to refactor our effects to use these operators.

Pipeable operators offer a mechanism of composing observable chains by using pure functions that can be used as standalone operators instead of methods on an observable. They’re lightweight and can decrease your overall build size by only including the operators being used. See reference section for further reading on this.

Sample effect -

//game-play.effects.ts
...
@Effect()
startNewGame$ = this.actions$
.ofType(GamePlayActionTypes.CREATE_NEW)
.pipe(
switchMap((action: gameplayactions.CreateNewGame) =>
this.svc.createNewGame(action.payload.gameOptions, action.payload.user).pipe(
map((gameId: string) => new gameplayactions.CreateNewGameSuccess(gameId))
//catchError(error => new)
)
)
);
...

router-store

Our application currently stores only the domain entities in the store. The router-store allows us to store the url and the router state in the store based on a route change action on the angular router. We can then have effects listen to these actions and invoke our services to load data as needed. The components would no longer need to listen to route changes, but simply observe on the store for changes.

A great explanation of the states of our application is covered by Victor Savkin in his post - https://blog.nrwl.io/using-ngrx-4-to-manage-state-in-angular-applications-64e7a1f84b7b.

The video series by Todd Motto listed in the reference section is also a great resource to understanding how this all comes together.

Let’s start by adding the router state and a custom serializer to our reducer -

//reducers/index.ts
import ...
export interface RouterStateUrl {
url: string;
queryParams: Params;
params: Params;
}
export interface State {
routerReducer: fromRouter.RouterReducerState<RouterStateUrl>
}
export const reducers: ActionReducerMap<State> = {
routerReducer: fromRouter.routerReducer
}
export const routerState = 
createFeatureSelector<fromRouter.RouterReducerState<RouterStateUrl>>('routerReducer');
export class CustomSerializer implements fromRouter.RouterStateSerializer<RouterStateUrl> {

serialize(routerState: RouterStateSnapshot): RouterStateUrl {
const {url} = routerState;
const {queryParams} = routerState.root;
  let state: ActivatedRouteSnapshot = routerState.root;
while (state.firstChild) {
state = state.firstChild;
}
    const {params} = state;
return { url, queryParams, params };
}
}

We can then list our CustomSerializer as a provider for RouterStateSerializer in the app.module.


Using router actions in effects

We can now observe on these router actions in our effects to load data.

//game.play.effects.ts
...
//load from router
@Effect()
// handle location update
loadGame$ = this.actions$
.ofType('ROUTER_NAVIGATION')
.map((action: any): RouterStateUrl => action.payload.routerState)
.filter((routerState: RouterStateUrl) =>
routerState.url.toLowerCase().startsWith('/game-play/') &&
routerState.params.gameid
)
.pipe(
switchMap((routerState: RouterStateUrl) =>
this.svc.getGame(routerState.params.gameid).pipe(
map((game: Game) => new gameplayactions.LoadGameSuccess(game))
)
)
);
...

As we can see above, we can look for the game-play route to load our game. We can remove the dispatch action for the route from our component.


We’ll go ahead and implement these changes across the application. We’ll add stores for core and bulk feature modules. The complete code for this part is here.


In this part, we refactored our store into smaller feature module stores and used the router-store to manage our route states. We also fixed some bugs and made UI changes to cleanup our bulk upload.

Next Up

Two player game play.


If you enjoyed this article please recommend and share and feel free to provide your feedback on the comments section.