This blog is intended for developers who are familiar with building React apps with Redux. In this blog, I would like to share one of the challenges I faced when working with Redux combine reducers and how I overcame it.
Problem
When working on my recent project, I employed “reducer separation” strategy enabled by combine reducers to allocate responsibility for each of the reducers to manage independent subtree of the application’s state. This has resulted in a small, testable and easily manageable reducers. Each of the reducers received only subtrees of the whole state tree, without even being aware of the other parts of the state. Although this is an ideal functionality, sometimes we want to derive new parts of the state based on other parts of the state, in other words, we want to access a slice of state in other reducers and this is not possible since combine reducers adheres to the single responsibility principle. In this blog, we will see how I overcome this restriction by writing my own combine reducer.
SEEING PROBLEM WITH EXAMPLE
The problem mentioned above is demonstrated with the following example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { combineReducers } from 'redux'; | |
import bookReducer from './book'; | |
import reviewReducer from './review'; | |
const rootReducer = combineReducers({ | |
book: bookReducer, | |
review: reviewReducer | |
}); | |
export default rootReducer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const book = (() => { | |
return { | |
books: [] | |
}; | |
})(); | |
const bookReducer = (state=book, action) => { | |
console.log(state); //=> { books: [] } | |
//logic here | |
}; | |
export default bookReducer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const review = (() => { | |
return { | |
reviews: [] | |
}; | |
})(); | |
const reviewReducer = (state=review, action) => { | |
console.log(state); //=> { reviews: [] } | |
//logic here | |
}; | |
export default reviewReducer; |
What is happening in this example?
We have two parts of the application state, review
and book
and they are managed by reviewReducer
and bookReducer
, respectively. When actions are dispatched, Redux will invoke each of the reducers and provide its respective parts of state(as inspected from the logs).
As you can see, accessing both review
state and book
state in any of these reducers is not possible since we are using Redux’s combine reducers. But what if need access to review
data from the bookReducer
function? Since combine reducers are just functions, we can create our own custom combine reducer to pass the necessary data to any reducers.
Custom Combine Reducer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bookReducer from './book'; | |
import reviewReducer from './review'; | |
const rootReducer = (state={}, action) => { | |
return { | |
review: reviewReducer(state.review, action), | |
book: bookReducer(state.book, {…action, …state.review}) | |
}; | |
}; | |
export default rootReducer; |
The implementation of our custom combine reducer is very similar to the Redux’s combine reducer. All it does is create a new state object by combining the previous state and the output of each of the sub-reducers. In order to provide review
data to bookReducer,
we are intercepting the action and adding thereview
data before calling the bookReducer,
thus providing it with necessary data.
Custom combine reducer pattern works well but if you find yourself in a situation pass data to across many reducers frequently, then it is advisable to merge related data and handle them together.
Code to the above example can be found in my Github repository: https://github.com/ahamedali95/redux-custom-combiner-reducers