Embracing Higher-Order Reducers
Here's a quick story about how I used Higher-Order Reducers to add a new feature to my D&D Initiative Tracker application, whilst keeping the code maintainable and reusable.
I was adding a feature allowing the user to clear any of their inputted data. It's quite a simple job - render a button, raise an action when the button is clicked and then handle the action in each reducer that contains user data. But this is duplication, as in each reducer you're effectively performing the same piece of work. This seems like a code smell and I started investigating ways to remedy this.
In functional programming terms, a higher-order function is a function whose return type is another function, or whose arguments can be functions. Languages that support this concept treat functions as first-class citizens - they're just another type that can be passed around, like an int, string or object. It follows that a higher-order reducer is one which returns a reducer, or accepts reducers as arguments.
One use of higher-order reducers is to wrap a reducer, to extend its functionality. Let's take a look at a code sample:
const persistable = (defaultState, wrappedReducer) => {
return (state = defaultState, action) => {
if (action.type === 'CLEAR_PERSISTENCE') {
return defaultState;
}
else {
return wrappedReducer(state, action);
}
}
}
The outer function returns a reducer function, which can be included in our application via a combineReducers
call, just like any other Redux reducer. We're passing two arguments to this outer function call - the default state, and a reducer we'd like to augment with extra behaviour.
A single reducer is returned, but now it handles an extra action - when CLEAR_PERSISTENCE
is raised we return the initial default state. Other actions are passed through to the reducer we've wrapped. This is essentially the Decorator Pattern from OOP.
So let's see how we can wrap an existing reducer with our persistable
reducer:
const defaultState = { name: '' };
const nameReducer = (state, action) => {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.value };
default:
return state;
}
};
const persistableReducer = persistable(defaultState, nameReducer);
export default persistableReducer;
The concept of a persistable reducer is now entirely re-usable. We can call it multiple times, passing in a different reducer each time, to augment each of those with the common behaviour. Equally, the code to manage what happens exists in one place.