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.
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)
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!