I have a cart reducer function with add, update and delete cases. I also have a product array within the redux store. When there are two items added to the product array, instead of having two items, I increment the quantity value. My main question is, should the reducers include any logic i.e. determine if the products array already contains the exact product and just returns an update on quantity of product or should this behavior be handled within the presentational component checking for existing products and either adding a new product or updating the quantity?
function CartReducer (state = initialState, action) {
switch (action.type) {
case AddToCart:
return {
...state,
products: [...state.products, action.product],
totalPrice: state.totalPrice += (action.price * action.quantity)
}
case RemoveItemCart:
return {
...state,
products: [
...state.products.slice(0, action.index),
...state.products.slice(action.index + 1)
]
}
case UpdateItemQuantity:
return {
...state,
products: state.products.map((product, index) => {
if (index === action.index) {
return Object.assign({}, product, {
quantity: action.quantity
})
}
return product
})
}
default:
return state
}
}
Per the Redux FAQ entry on splitting logic between reducers and action creators:
There's no single clear answer to exactly what pieces of logic should go in a reducer or an action creator. Some developers prefer to have “fat” action creators, with “thin” reducers that simply take the data in an action and blindly merge it into the corresponding state. Others try to emphasize keeping actions as small as possible, and minimize the usage of getState() in an action creator. (For purposes of this question, other async approaches such as sagas and observables fall in the "action creator" category.)
There are some potential benefits from putting more logic into your reducers. It's likely that the action types would be more semantic and more meaningful (such as "USER_UPDATED" instead of "SET_STATE"). In addition, having more logic in reducers means that more functionality will be affected by time travel debugging.
This comment sums up the dichotomy nicely:
Now, the problem is what to put in the action creator and what in the reducer, the choice between fat and thin action objects. If you put all the logic in the action creator, you end up with fat action objects that basically declare the updates to the state. Reducers become pure, dumb, add-this, remove that, update these functions. They will be easy to compose. But not much of your business logic will be there. If you put more logic in the reducer, you end up with nice, thin action objects, most of your data logic in one place, but your reducers are harder to compose since you might need info from other branches. You end up with large reducers or reducers that take additional arguments from higher up in the state.
I also wrote my own thoughts on "thick and thin" reducers:
There's valid tradeoffs with putting more logic in action creators vs putting more logic in reducers. One good point that I saw recently is that if you have more logic in reducers, that means more things that can be re-run if you are time-travel debugging (which would generally be a good thing).
I personally tend to put logic in both places at once. I write action creators that take time to determine if an action should be dispatched, and if so, what the contents should be. However, I also often write corresponding reducers that look at the contents of the action and perform some complex state updates in response.
update
As of 2020, we specifically recommend putting as much logic as possible in reducers:
Wherever possible, try to put as much of the logic for calculating a new state into the appropriate reducer, rather than in the code that prepares and dispatches the action (like a click handler). This helps ensure that more of the actual app logic is easily testable, enables more effective use of time-travel debugging, and helps avoid common mistakes that can lead to mutations and bugs.
There are valid cases where some or all of the new state should be calculated first (such as generating a unique ID), but that should be kept to a minimum.