This article builds on the prior series showing how to create an application using Angular 2 or later, and leverage ngrx store for application state. The article will extend the same project state by replacing the reducer function for each store that uses a switch statement, to a dedicated class for each action type and put them all together consumable by the central Ngrx store.
You may be familiar with the typical Ngrx state reducer with a switch block for each action type. If not read up on the prior articles in this series. The full source of the reducer modified in this article can be found at this commit.
https://github.com/michael-lang/sample-ng2-mvc/blob/a85e78a55fe115d1b4567ae1ab4bcafee7d7d8c1/SampleAngular2Mvc/app/person/person.store.ts
The key section we want to change defines a switch on the type of action containing a case for each possible action to run against a “person state” as required by this application. It depends on constants defined above this function in the file defining each of the case statements.
export const PersonActionTypes = { SEARCH: type('[Person] Search'), // other types omitted from this snippet for each case INSERT_COMPLETE: type('[Person] Insert Complete') } export function PersonReducer(state = initialState, action: PersonActions): PersonState { switch (action.type) { case PersonActionTypes.SEARCH: return tassign(state, { ... } case PersonActionTypes.SEARCH_COMPLETE: return tassign(state, { ... } case PersonActionTypes.SEARCH_CRITERIA_CHANGE: return tassign(state, { ... } case PersonActionTypes.OPEN: return tassign(state, { ... } case PersonActionTypes.CLOSE: return tassign(state, { ... } case PersonActionTypes.INSERT_COMPLETE: return tassign(state, { ... } case PersonActionTypes.UPDATE_COMPLETE: return tassign(state, { ... } } }
This prior version of the code also used classes for each type of action so that components would have a typesafe way of dispatching state changes and know that it passed in the correct payload type. This works well, but is heavier than it needs to be.
export class PersonInsertAction implements Action { type = PersonActionTypes.INSERT_COMPLETE; constructor(public payload: PersonHolder) { } } export type PersonActions = PersonSearchAction // other types omitted from this snippet for each case | PersonInsertAction;
The type safety of the classes defined as used in the switch case above is useful, but it seems to repeat the pattern of defining the same actions multiple times for different reasons. The first step is to move the reducer logic for each action into the relevant action class.
export class PersonInsertAction implements Action { type = PersonInsertAction.type; constructor(public payload: PersonHolder) { } static type: string = type('[Person] Insert Complete'); static reduce(state: PersonState, action: PersonInsertAction) { let oldId = action.payload.PlaceholderId; action.payload.PlaceholderId = action.payload.Person.PersonId; return tassign(state, { results: [action.payload.Person].concat(state.results), //add to top openList: state.openList.map(i => i.PlaceholderId === oldId ? action.payload : i) //TODO: if this was the active tab, the tab id is being updated, so the 'activeTabId' should update to match }); } }
The logic in the static reduce function here is the code that was originally in that case of the reducer switch statement. This new code retains the safety of not allowing duplicate action types in the application using the ‘type(..)’ function in the declaration of the static type property. The class is also required to declare an instance ‘type’ property to meet the interface of the ngrx Action class, and a constructor taking the payload for the convenience (type safety) of the component code that dispatches the state change. The static property ‘type’ and function ‘reduce’ are required by the buildReducer function that combines the reducers of these classes into a single reducer used by ngrx.
export const PersonReducer = buildReducer(initialState, PersonSearchAction, PersonSearchCompleteAction, PersonSearchCriteriaChangeAction, PersonSearchResetAction, PersonOpenAction, PersonTabActivateAction, PersonCloseAction, PersonInsertAction, PersonUpdateAction );
This reducer accepts an initialState previously passed into the prior version of the reducer function, and a list of the class types to be called by the new reducer. The buildReducers function is not part of ngrx code, rather it is defined in this sample project.
/** * This function builds a state reducer to replace the typical switch/case pattern, * given an initial state and a list of classes with static type and reduce function. * @param initial The initial state for this reducer, called by store to initialize the state * @param actionClasses a list of classes (type names) implementing the required static reducer interface. */ export function buildReducer<T>(initial: T, ...actionClasses: { type: string, reduce: (state: T, action: Action) => T }[]) { let handlers: { [key: string]: (state: T, action: Action) => T } = {}; actionClasses.forEach((ac) => { handlers[ac.type] = ac.reduce; }); return (state: T = initial, action: Action) => handlers[action.type] ? handlers[action.type](state, action) : state; }
The buildReducer function uses generics to define the type ‘T’ of the initial state and thus of the state type passed into and returned from each reducer. The return statement in this function assumes you may have other stores in your application not handled by this combined reducer by only acting on the action types defined in this handler array, and for the action type names handled by this reducer the ‘reduce’ function is called on the relevant class, and only on that class.
The final code after these changes can be found at this commit.
Future versions of this code are now free to move the action classes into separate files. Future versions of this project may take advantage of that benefit.
Full Source code for this article series
https://github.com/michael-lang/sample-ng2-mvc
How to build a real world Angular app with ngrx store, Part I: Define the state
How to build a real world Angular app with ngrx store, Part II: Components using Store
Hi Michael,
I get an AOT build problem using the buildReducer in Angular 4, ngrx 4. Have you faced some an issue?
ERROR in Error encountered resolving symbol values statically. Calling function ‘buildReducer’, function calls are not supported.