If you’ve been using Redux for long, you’ve probably used its combineReducers function to create your application’s root reducer. But you may not have thought of using it in your sub-reducers as well. Let’s see how we can take advantage of combineReducers to simplify our code.

Introduction

If you’ve worked on a Redux app of any size, you’re probably familiar with combineReducers. The most common use case is to create your root reducer with it.

For example, the Async Example in the Redux documentation contains the following rootReducer definition:

Root reducer
const rootReducer = combineReducers({
postsByReddit,
selectedReddit
})
export default rootReducer

This usage of combineReducers is pretty common and well-documented.

But have you thought about using combineReducers for the lower levels of your state tree as well? Let’s look at some examples.

Async Example

Here’s the entire reducer file from the same Async Example:

Async Example
import { combineReducers } from 'redux'
import {
SELECT_REDDIT, INVALIDATE_REDDIT,
REQUEST_POSTS, RECEIVE_POSTS
} from '../actions'
const selectedReddit = (state = 'reactjs', action) => {
switch (action.type) {
case SELECT_REDDIT:
return action.reddit
default:
return state
}
}
const posts = (state = {
isFetching: false,
didInvalidate: false,
items: []
}, action) => {
switch (action.type) {
case INVALIDATE_REDDIT:
return {
...state,
didInvalidate: true
}
case REQUEST_POSTS:
return {
...state,
isFetching: true,
didInvalidate: false
}
case RECEIVE_POSTS:
return {
...state,
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
}
default:
return state
}
}
const postsByReddit = (state = { }, action) => {
switch (action.type) {
case INVALIDATE_REDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return {
...state,
[action.reddit]: posts(state[action.reddit], action)
}
default:
return state
}
}
const rootReducer = combineReducers({
postsByReddit,
selectedReddit
})
export default rootReducer

Let’s look at the posts reducer a little more closely. Notice that its initial state is an object with three keys and that the handler for RECEIVE_POSTS adds in a fourth.

What if we split posts into pieces that we re-assemble with combineReducers?

Splitting posts
const isFetching = (state = false, action) {
switch (action.type) {
case REQUEST_POSTS:
return true
case RECEIVE_POSTS:
return false
default:
return state
}
}
const didInvalidate = (state = false, action) {
switch (action.type) {
case INVALIDATE_REDDIT:
return true
case REQUEST_POSTS:
case RECEIVE_POSTS:
return false
default:
return state
}
}
const items = (state = [], action) {
switch (action.type) {
case RECEIVE_POSTS:
return action.posts
default:
return state
}
}
const lastUpdated = (state = null, action) {
switch (action.type) {
case RECEIVE_POSTS:
return action.receivedAt
default:
return state
}
}
const posts = combineReducers({
isFetching,
didInvalidate,
items,
lastUpdated
})

Notice how the individual reducers and their initial states are much simpler? I find that it’s much easier to write my reducers this way, and its also much easier to understand them. I can think about each section of the state tree independently of the others, and I don’t have to do any complicated merging or object construction.

I should point out one slight change. In the original code, the initial value of the lastUpdated key is undefined because it doesn’t appear in the initial state. When I split out a separate reducer for that key, I had to change the initial value to null. This is because Redux warns you if a reducer produces undefined as its result. Normally when that happens, it’s an error, so it warns you about it.

There are a few disadvantages to this decomposition approach, though, and you’ll have to weigh those against the simpler code.

  • It’s a bit harder to see the overall shape of the state tree at a glance. You can see all of the keys in the state by looking at the combineReducers call, but to see what the value of each key looks like, you have to look at the individual reducers. I haven’t found this to be a big deal. In fact, writing my reducers this way means that I don’t really need to think about the overall shape of the state tree as much as I used to.

  • I have to look in more places to see all of the effects of a single action. Notice that all four of the individual reducers handle the RECEIVE_POSTS action. If I keep all of the individual reducers in the same file, it’s not too bad, but if I split them into different files, it gets harder. However, this is also the case if you use combineReducers at a higher level. Again, I haven’t found this to be a big problem, because I tend to think about each sub-section of the state tree independently now.

  • There are more lines of code now. This will be true almost any time you extract functions out of a bigger function. In my opinion, the simpler code makes this a good tradeoff. I always use a createReducer function as described in the Reducing Boilerplate section of the Redux documentation to make this overhead smaller. The next example will show what this looks like.

Error Messages

My current project is a React Native application. The application has some background API calls going on. If one of those fails, we want to report the error in an animated slide-out panel.

We represent this in the state tree with a simple object containing a hasError key and an errorMessage key. Normally, we could just have the errorMessage and show or hide the error panel accordingly, but because the panel slides in and out, we need the error message to stay in place until the animation completes. Thus, we control the panel with the hasError flag and keep the errorMessage around.

Here’s the reducer code we originally wrote for this:

Original reducer
const initialState = {
hasError: false,
errorMessage: ''
}
const clearError = state => ({
...state,
hasError: false
})
const showError = (state, { payload }) => ({
...state,
hasError: true,
errorMessage: payload
})
export default createReducer(initialState, {
[ActionType.CLEAR_ERROR]: clearError,
[ActionType.SHOW_ERROR]: showError
})

After applying combineReducers, we ended up with:

With combineReducers
const hasError = createReducer(false, {
[ActionType.CLEAR_ERROR]: () => false,
[ActionType.SHOW_ERROR]: () => true
})
const errorMessage = createReducer('', {
[ActionType.SHOW_ERROR]: (state, { payload }) => payload
})
export default combineReducers({
hasError,
errorMessage
})

Notice how much less code there is in both examples because of the use of createReducer.

But the refactored example is even shorter than the original because the handler functions became so simple that I was able to inline them right into the createReducer calls. Before refactoring, they were long enough that I felt it better to extract them to their own named functions.

Conclusion

I encourage you to take a look at your reducers and see if you can simplify them by applying combineReducers at lower levels of your state tree.

I typically look for cases where the initial state is an object; these are good candidates for this refactoring. However, if the object is really a domain entity (like a user or a todo, for example), I typically won’t decompose the reducer like this. But in all other cases, where the object is a collection of semi-independent keys, I’ll think seriously about applying this refactoring.

The Redux documentation has an entire section on Structuring Reducers that talks about this technique and many others. It’s well worth taking the time to read through it.