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:

Reducer factory for list reducers

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.

Reducer factories for reducers

We can now create list reducers for specific action types by calling createList(actionTypes). They can be used as shown in the test below.

A test of the createList factory

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:

  1. A predicate that takes an action and tells if the reducer should be applied.
  2. A function that maps the action to the key.
A higher-order reducer for splitting reducers by keys

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.

Test of createByKey and selectors

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.

Composable functions to be used in createFilter

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.

Higher order function for creating filter reducers

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

Test of createFilter

We can use createList together with createFilter by putting the reducer returned from createList as filter reducers to createFilter.

Using createFilter and createList together

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.

A higher-order reducer to create a reducer object for createFilter

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 .

Composing createFilter, createFilterReducers and createList

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.

A function to apply enhancers to an object with 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.

Composing enhanceReducers with createFilter and createFilterReducers

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.

A function for creating a filter reducer from filterNames and filterEnhancers

We can test it with the same reducer and enhancer as before.

Testing createEnhanedFilter

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.

A higher-order reducer for splitting by a filterQuery

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.

Selector factory for filter reducers with enhancers

To put it all together, we will composecreateEnhancedFilter with createList and use a filterQuery enhancer for one of the filters.

Creating a filter reduer with a filter query enhancer

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.

Possible composition with pagination

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 :)