Ngrx without a switch statement

Ngrx without a switch statement

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.

The starting switch

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;

Replacing the switch

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.

https://github.com/michael-lang/sample-ng2-mvc/blob/9a2a05685ccbd8e232ada01bdc9bfef1a81ece40/SampleAngular2Mvc/app/person/person.store.ts

Moving Forward

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.



Further Reading

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

Evolving Angular to remember unsaved changes

About the Author

Michael Lang

Co-Founder and CTO of Watchdog Creative, business development, technology vision, and more since 2013, Developer, and Mentor since 1999. See the about page for more details.

3 Comments

  • Avatar By Stephen

    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.

    • Michael Lang By Michael Lang

      Yes, I have on another project using this code. We added another exported reducer wrapping the buildreducer call to get around the problem.

      //First change the reducer to no longer be exported… change the name of this now private reducer to ‘reducer’ instead of ‘PersonReducer’

      const reducer = buildReducer(initialState,
          PersonSearchAction,
          PersonSearchCompleteAction,
          PersonSearchCriteriaChangeAction,
          PersonSearchResetAction,
          PersonOpenAction,
          PersonTabActivateAction,
          PersonCloseAction,
          PersonInsertAction,
          PersonUpdateAction
      );
      

      //then… Need to use an exported function with our desired name, because AOT can not handle buildReducer.

      export function PersonReducer(state: PersonState, action: Action): PersonState {
        return reducer(state, action);
      }