Reusing Redux Reducers
Oftentimes in a Redux application we have a little bit of state that we'd like to be made re-usable. By state, I don't just mean the structure of some object, I'm also referring to the management of that state. This tends to happen as your application grows and then a requirement comes in to do something similar to what you've done previously. Very often you'll already have a nice bit of code that you'd like to re-use.
This recently happened to me, so I'd like to document how I go about making reducers re-usable in large-scale applications. Firstly, a brief recap.
What's a Reducer?
Recall that Redux applications contain a state object. The way that this state is modified is via a simple function: nextState = update(prevState, action)
An action is typically an object with an identifying type
property and a payload
. The type name takes the form of a unique String. A sample action looks like this:
const sampleAction = {
type: 'SAMPLE_ACTION',
payload: {
id: 1,
name: 'Hello There!'
}
};
This update
function is reducing the old state and the action into a new state object, which is why it's known as a reducer function. What you're actually wanting to re-use, therefore, are three things:
- The state model - the structure of the object corresponding to some application state
- Some actions - which define how the state can be mutated
- A reducer - which will perform the mutation on your state
A create Reducer function
What I normally do is have a function which, when called, returns a reducer function. This can then be included in your reducer tree via a combineReducers
call. In terms of actions - we need a unique name. To achieve this, the function accepts an actionRoot
argument - it's just a string! Internally, the function creates some action strings by prepending this value to each action name. This makes them unique, but within the function, they can be reference by their variable names (in this case: INCREMENT
and DECREMENT
). The action names are also returned too, in case they're needed elsewhere.
This is how this looks:
function createReducer = actionRoot => {
const INCREMENT = `${actionRoot}/increment`;
const DECREMENT = `${actionRoot}/decrement`;
const defaultState = {
val: 0
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case INCREMENT:
return {
val: state.val + 1
};
case DECREMENT:
return {
val: state.val - 1
};
default:
return state;
}
};
return {
INCREMENT,
DECREMENT,
reducer
};
}
export default createReducer;
Now, wherever we wish to use this state model, we can simply call the function!
import createReducer from './createReducer';
import createAction from 'redux-actions';
const { INCREMENT, DECREMENT, reducer } = createReducer('my-sample-counter');
export const incrementCounter = createAction(INCREMENT);
export const decrementCounter = createAction(DECREMENT);
export default reducer;
In this example, I'm using the redux-actions
library to create action creator functions based on the actions returned by my createReducer
function, so that when called, they return a standard action object of that type. These can now be used in a redux Connect
ed component via its mapDispatchToProps
function - let's say so that when a user clicks a button, either my INCREMENT
or DECREMENT
action is dispatched. I'm also returning the reducer so that it can be included in the state tree.
You'll have probably noticed that this concept is very similar in nature to what I discussed in my article on Higher-Order Reducers, and in yesterday's article on reusing Redux Form components. This shows the fantastic utility of composition!
Conclusion
As an application grows, you'll find yourself wanting to handle certain things in common ways - the state model for network requests, dialogs, and so on. This technique is a handy way of creating small modules that are ripe for re-use across your application. With this technique put in practice, you'll become familiarised with a common state model, and see a common set of actions that conform to a pattern in your dispatched action log.
Redux application states can become complicated to reason about. Reducing the cognitive overhead in doing so is imperative as your application grows in size and complexity. Overall, I think that this is an incredibly useful way of bringing down the cost of maintenance in a large-scale Redux project.