Reducer composition with Higher-Order Reducers
When working with reducers in Redux, some patterns tend to emerge and become repetitive. To tackle this problem, we can make use of higher-order reducers and reducer composition.
Higher-order reducers are functions that return reducers and/or take them as arguments. Reducers for lists are commonly needed throughout applications. A reducer factory for creating a list reducer could look like this:
Where actionTypes
is an object like:
actionTypes = {
SUCCESS: 'FETCH_SUCCESS’,
REQUEST: 'FETCH_REQUEST’,
INVALIDATE: 'FETCH_INVALIDATE’,
FAILURE: 'FETCH_FAILURE’,
}
We use combineReducers
to create the list reducer from reducers created by other reducer factories.
We can now create list reducers for specific action types by calling createList(actionTypes)
. They can be used as shown in the test below.
Creating a byKey enhancer
Now, let’s say we want to filter on a related resource, and store one list for each of those resources. We can create another higher-order function to take care of that. Let’s start with something more generic and create more specific implementations later.
Now, we can use createByKey
to split up a reducer by a key. It takes two arguments except for the reducer:
- A predicate that takes an action and tells if the reducer should be applied.
- A function that maps the action to the key.
Notice that we have separated the arguments, so that a function with a reducer as the only argument is returned. This is called currying and it means that we would call it with createByKey(pred, mapToKey)(reducer)
. We’ll get back to why this is useful.
We can test it with a simple reducer with an initial state of null
, that only cares about the SUCCESS
action type. It and returns the payload as the new state
if the action type is matching.
Combining reducers with a filter
It would also be nice if we could combine reducers with filtering. Let’s create a higher order reducer, that works similar to combineReducer
, but only calls the reducer when action.filterName
matches a key in the object with reducers.
In fact, this is pretty similar to what is provided by createByKey
. The main differences are:
- It takes an object as argument, with filter names as keys and reducers as values.
- It needs to compute and combine initial states of all reducers.
- It needs to use different reducers for every key.
- It knows how to get
filterName
from the action and how to define the predicate.
Maximizing composability
Remember that createByKey
returns a function with a reducer as only argument. We can create two higher-order functions that returns functions of a single argument, let’s say withInitialState(initialState)
and mapToReducer(mapActionToKey)
. If these functions take each others output as input, we can use compose
to combine them. If you‘re not familiar with compose
, it works like this:
compose(f, g, h)(x, y, z)
// is equivalent to
f(g(h(x, y, z)))
Let’s write these functions, and a helper function, combineInitialState
, that creates an initial state by combining the initial states of filterReducers
.
Some thing to watch out for here is mapToReducer
. It would throw if mapActionToKey
tries to access a nested property that doesn’t exist. However, we an use the predicate passed to createByKey
to make sure that won’t happen.
When putting it together, we can combine the previous functions with compose
. I have used has
from lodash
in the predicate, but it could be swapped to hasOwnProperty
.
A filtered combination of reducers can now be created by calling createFilter({ [SOME_FILTER]: reducer, ... })
. The keys will be matched against action.filterName
. The test below shows how we can create two filters for a simple reducer with the createFilter function
We can use createList
together with createFilter
by putting the reducer returned from createList
as filter reducers to createFilter
.
Composing with createFilterReducers
Now, if all we want is to create filters for the same reducer, we can create another function to take care of the burden. The arguments of this function should be an array of filter names and a reducer.
We can now use it with createFilter
and createList
. Since we have a set of single argument functions, that take each other as input, we can again use compose
.
For some filters, we might want to do something else before passing it to the filter reducer. Let’s write a function that takes an object with reducers, and an object with higher-order reducers as arguments, and returns a new object with enhanced reducers.
We can use it by adding as an argument between createFilter
and createFilterReducers
. The higher-order reducer, enhancer
is simply creating a nested state, with the state of the other reducer on the enhanced
key.
Preconfiguring with createEnhancedFilter
This composition may turn out to be a common use case, so we can create another function that takes reducerNames
and filterEnhancers
and does the composition.
We can test it with the same reducer
and enhancer
as before.
Putting it all together with createByFilterQuery
So, we already have createByKey
as an enhancer that we can use for related resources. However, it requires predicate
and mapActionToKey
as arguments, and those will often look very similar. If the id
of the related resource is represented as a { filterQuery: { [filterQueryKey]: id } }
, we can create a function to set upcreateByKey
in this way.
We’ll also add a selector factory to createFilter.js
. It takes an object with selectors as argument. If filterArgs.filterName
matches a key in the selectors, it will call that selector with the filterState
before passing the result back. It can help us deal with nested states from enhancers.
To put it all together, we will composecreateEnhancedFilter
with createList
and use a filterQuery
enhancer for one of the filters.
This pattern can be used to put together other kinds of reducers like undo
and pagination
. I will end with an example of what that could look like. We have three filters, they will all refer to paginated lists. Two of the filter reducers have enhancers to handle related resources. For FILTER_CREATED_YEAR
, we have nested a nested enhancer just for the sake of it.
I believe this allows good code re-use when creating other reducers and higher-order reducers, rather than re-purposing. They are composed from the ground up by rather simple reducers with single concerns. Most of the higher order reducers take the approach of wanting to know as little as possible about things they don’t need to care about.
How do I do this? I don’t know, I really don’t want to know. You go figure it out!
— Higher-order reducer
Now, I don’t know if using compose
everywhere is madness or not. It might take a bit of getting used to, but it certainly feels good :)