Ngrx Metareducer

Sep 4th, 2020 - written by Kimserey with .

When building Angular application with Ngrx, it is helpful to see the action flowing into our states for debugging. The quick and easy way to debug is to make use of the Redux DevTools which shows the list of actions and provide time travelling functionalities. Another way is to simply log the action, the state prior applying the action, and the resulting state. In Ngrx, we are able to do that using a Meta-reducer.

Reminder on Ngrx Reducer

A reducer is defined by the ActionReducer<T> interface:

1
2
3
export interface ActionReducer<T, V extends Action = Action> {
    (state: T | undefined, action: V): T;
}

It is a callable taking a state and action as arguments and returning the resulting state after the action has been applied.

To register our reducer, we use the StoreModule.forRoot module registration:

1
2
3
4
5
6
export declare class StoreModule {
    static forRoot<T, V extends Action = Action>(
        reducers: ActionReducerMap<T, V> | InjectionToken<ActionReducerMap<T, V>>,
        config?: RootStoreConfig<T, V>
    ): ModuleWithProviders<StoreRootModule>;
}

forRoot takes two arguments, the reducers in the form of ActionReducerMap or InjectionToken<ActionReducerMap> and an optional config. The InjectionToken is preferable as it is AOT friendly so if we have a reducer named router, we can create the injection token:

1
2
3
4
5
6
7
8
9
10
11
12
export interface State {
  router: RouterReducerState<any>;
}

export const rootReducer = new InjectionToken<ActionReducerMap<State>>(
  'Root reducers token',
  {
    factory: () => ({
      router: routerReducer,
    }),
  }
);

and register it:

1
StoreModule.forRoot(rootReducer)

Implement a Meta-reducer

As we just saw, a reducer is a callable taking a state and action and returning the new state. As opposed to that, a Meta-reducer is a special reducer which wraps a reducer.

1
2
export declare type MetaReducer<T = any, V extends Action = Action> = 
    (reducer: ActionReducer<T, V>) => ActionReducer<T, V>;

It is a type alias to a function taking a reducer as argument, and returning a reducer as output. Meta-reducer can be passed as part of the optional configuration RootStoreConfig<T, V>; the second argument of fromRoot.

For example a no-op Meta-reducer for demonstration purposes would be:

1
2
3
4
5
6
7
8
/**
 * Does nothing.
 */
export function NoOp(reducer: ActionReducer<State>): ActionReducer<State> {
  return (state: State, action: any): any => {
    return reducer(state, action);
  };
}

and we can then register it in

1
StoreModule.forRoot(rootReducer, { metaReducers: [NoOp] })

What this Meta-reducer does is simply taking the action and applying it to the state and returning the new state which is essentially what would already happen if it wasn’t registered.

But from this example, we can see how we can take advantage of the Meta-reducer to create a logger Meta-reducer which would log the action, the previous state, and the resulting state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * Logger reducer used for debugging to log state changes in console.
 */
export function logger(reducer: ActionReducer<State>): ActionReducer<State> {
  return (state: State, action: any): any => {
    const result = reducer(state, action);
    console.groupCollapsed(action.type);
    console.log('prev state', state);
    console.log('action', action);
    console.log('next state', result);
    console.groupEnd();
    return result;
  };
}

and we can then register it:

1
StoreModule.forRoot(rootReducer, { metaReducers: [logger] })

This will result in every action being logged with previous and next state!

External Sources

Designed, built and maintained by Kimserey Lam.