DEV Community

Kriti Rai
Kriti Rai

Posted on

Combining Reducers

I was recently working on a lab in building a Yelp-like application that uses React and Redux to add and delete restaurants and their reviews.

app gif

While working my way through the lab I found that my reducer function, manageRestaurants, was dense. So, I naturally sought to split my giant reducer function into two children reducer functions so that each function was responsible for only one resource's state. Using combineReducers, I then combined the children reducers in one parent reducer function, rootReducer, which is what gets passed to the store. This not only made my code cleaner but also much easier to debug.

Finally, I got the app working in the browser just as the lab wanted and before I could take that big sigh of relief I found that the tests were failing. The lab just wanted us to create one reducer function and put all reducer logic in there. Ugh!

office space

Regardless, I decided to create a separate branch and push up my clean and amazing code there and reverted my master branch to the old way to pass the tests. In so doing, however, I realized that now I had a greater understanding of how combineReducers works. Additionally, now that I had seen both scenarios I could use that knowledge and experience to decide when I could use combineReducers. If you are just working with one or two resources, maybe you don't quite need to use this helper function. However, imagine a big app with multiple resources and soon enough you will find yourself tangled up in a number of switch statements and a big, fat state with multiple key-value pairs.

Refactoring with combineReducers

All talks aside, lets look at my giant reducer manageRestaurants first, which is maintaining the state of both restaurants and reviews.

reducer

Now, let's split our giant reducer into two child reducer functions, say restaurantReducer and reviewReducer. The former manages the state of the restaurants whereas the latter manages the state of the reviews.

restaurantReducer

restaurantReducer

reviewReducer

reviewReducer

Now, here's our rootReducer, where we will call our children reducer functions. Notice, we imported combineReducers from redux.

rootReducer

rootReducers

This is equivalent to writing:

function rootReducer(state = {}, action) {
  return {
    restaurants: restaurantReducer(state.restaurants, action),
    reviews: reviewReducer(state.reviews, action),
  };
};

This basically produces the same giant reducer function as manageRestaurants does but in a much more abstract and cleaner way.

Conclusion

If your app is big and has more than one or two resources, you might be better off splitting up the state object into slices and using a separate child reducer to operate on each slice of the state. The slices of state can then be combined using combineReducers, a helper utility lent by Redux, in a parent reducer, conventionally named rootReducer. Keep in mind that using combineReducer might not be helpful if one is intending to learn what is going under the hood as it abstracts the way reducers are being combined and working together. So, try to play around with both scenarios to get a better understanding of how reducers work and when to use combineReducers.

Discussion (4)

Collapse
markerikson profile image
Mark Erikson • Edited on

Nice post! Got a couple tips for you.

First, I see that you've got this line in the reducer:

id : cuidFn()

cuid generates random IDs. Ideally, reducers should never be doing anything that involves randomness, because it means they are no longer "pure". Pure functions should always return the exact same result when given the same inputs. A reducer that produces random numbers or IDs would be returning a different result each time it was called.

Now, in reality, generating random IDs in a reducer is unlikely to meaningfully break anything except in rare cases, like doing time-travel debugging. Still, it's a good practice to avoid randomness in reducers, same as you'd avoid mutating state. If possible, try to put that random number generation where you're creating the action, when you dispatch it. (If you're interested, a while back I wrote a post on experimenting with sorta-kinda generating repeatable random numbers in a reducer.)

Also, as a side note: I don't think you needed to assign const cuidFn = cuid. The default export of the cuid library is just that function - you can do import cuid from "cuid", and then just call it directly like const id = cuid(). I also don't think you need parentheses around (combineReducers) either.

On the topic of combineReducers, we've got a Redux docs page that specifically has some more information on using combineReducers effectively.

Finally, I'd encourage you to check out our new Redux Starter Kit package. It includes utilities to simplify several common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once without writing any action types or action creators by hand:

redux-starter-kit.js.org

Once you're familiar with the core ideas of using Redux, the RSK package can really help you simplify a lot of your code.

Hope that helps. Keep up the good writing, and lemme know if you've got any questions I can help with!

Mark Erikson
Redux maintainer

Collapse
kritirai profile image
Kriti Rai Author

Mark, thank you for such an amazing feedback. It means a lot. The lines including cuid were given by the lab and I did not pay much attention to tweaking them (maybe I can transfer that over to the react component to deal with it) but I will keep your advice in mind while working with reducers. Thanks for the docs and RSK package reco. I will make sure to check them out. Cheers!

Collapse
jfn0 profile image
jfn0

Great!, We started using it too. We pass from a giant reduser To a more DDD approach where each combined reducer act as a domain.

Collapse
kritirai profile image
Kriti Rai Author

Great way to clean up your code and it made debugging easier. :)