Problem
I'd like to chain multiple high order reducers using ngrx, so that similar parts of code in my reducers have only one implementation.
My application has number of pages that have quite similar functionalities. Their reducers also look quite similar. For this example let's consider an application with three pages: Page One, Page Two and Page Three. Each of these pages contains a counter, but is allowed to do different things with it. And so:
- Page One can increment and decrement value of counter
- Page Two can increment, decrement and reset value of counter
- Page Three can increment and reset value of counter
Example of such application can be found here. It is very naive implementation with separate reducer for each page - a lot of very similar functionalities are repeated in each reducer.
Solution with only one high order reducer
I managed to move part of common logic to separate piece of code using high order reducer. In this case it is increment functionality:
interface Actions {
incrementAction: ActionCreator<string>;
}
export const withIncrementation = ({ incrementAction }: Actions) => (
initState,
...actions
) =>
createReducer(
initState,
on(incrementAction, state => ({
...state,
counter: state.counter + 1
})),
...actions
);
This high order reducer can be used like this:
const pageOneReducer = withIncrementation({
incrementAction: PageOneActions.IncrementRequested
})(
initialState,
on(PageOneActions.DecrementRequested, (state: State) => ({
...state,
counter: state.counter - 1
}))
);
export function reducer(state: State | undefined, action: Action) {
return pageOneReducer(state, action);
}
Up to this point everything works fine. Application that works correctly like this is here
Problem with making multiple high order reducers work
Problem starts, when I try to chain multiple high order reducers to work. In example application both Page Two and Page Three are capable of resetting counter (and incrementing too), so I'd like to use 2 high order reducers now. I prepared new high order reducer, very similar to the previous one, that should do the work:
export const withReseting = ({ resetAction }: Actions) => (
initState,
...actions
) =>
createReducer(
initState,
on(resetAction, state => ({
...state,
counter: initialState.counter
})),
...actions
);
It works fine when it's alone, however I can't find a way to make it work in such way, that both withIncrementation and withReseting work fine on the same reducer. Example application where I tried to achieve that can be found here, but it does not work (it seems like my state stops working at all).
Naive approach with 3 separate reducers
I came across your question because I was trying to implement something similar within my application. I am accessing a lot of entities from my backend API, and all are similar and require similar reducers.
Your post was helpful, as it showed me an approach that would not work: that is, chaining the reducers. That got me thinking: what if we stick with a single reducer, but generated the
ons()with a higher-order function?Helper Type
First, I created a little Action helper type to make it easier to configure Actions with parameters:Higher-Order Function
Then I created my higher-order function. For this example, we will generate three Action Reducers, two of which will have parameters. Your functions can contain as many or as few Action Reducers as you need. Note that I am using a generic State type, which as you would expect means that all reducers that use this function must share a similar State. Obviously, you could also just hard-code a specific State type here as well.The Reducer
Finally, we come to actually creating the reducer. This ends up looking pretty much like normal (this is assuming you've already defined an
initialState). What's cool here is, you can easily mix and match multipleon()generation functions (which I believe matches your initial scenario perfectly), and of course create one-offon()s as usual.Note: you can even pass-in an instance of
EntityAdapter<MyType>from@ngrx/entity(if you're using this library) via your config interface and use all theadapter()methods as well.