Source:  Twitter logo

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.

48 users liked answer #0dislike answer #048
markerikson profile pic
markerikson

Most certainly! Reducers should be pure functions, so the logic must be pure as well. This means there should be zero side effects. Side effects would include (but not limited to):

  • Database requests/stores
  • File IO
  • REST/async calls
  • Global or external mutations
  • Data mutations of any sort

So reducers should never mutate the state that comes in, but return a copy with modifications only made to the copy, if that. Immutable values (like strings, numbers, undefined, etc) can be returned as-is, and so can state, if it hasn't been modified. If you need to make any changes to any inputs, however, you'd return a new copy or a new value.

With regard to the logic being called from your reducer, as long as all that code meets these requirements, then you are in-line with the Redux pattern.

Unfortunately, JavaScript doesn't have a way to know for sure when any given code has side effects (other languages do), so you should just be aware of what you are calling.

Will it break Redux if you fail? No. But things might not work exactly as you (or the Flux/Redux developers) expect.

12 users liked answer #1dislike answer #112
Brian Genisio profile pic
Brian Genisio

No, they shouldn't contain logic

Actions gather and emit large objects that reducers split into proper locations

Instead of large objects, you may procedurally dispatch actions at each step in an action creator, which is what the top answer seems to promote (yet is impossible with plain redux)


  • Components: pass existing redux data into actions

  • Actions: gather data and emit large object, applicable to as many reducers as possible

  • Reducers: extract action object for granular, non-repeating, flat storage (as opposed to monolithic, redundant, nested folders). I recommend explicit initialState for easy maintenance and blatantly obvious data shape (states should never change shape; if joined objects are unavailable, serialize by id rather than sometimes nesting full objects).

    An id can be thought of as a serialized memory reference or "pointer" which may have different content but can be === to itself; the id is the object reference.. serialized.

  • *Side Effects: subsequent action listener/dispatchers, useful when you don't want to wait before emitting an initial event (sagas are subscription-based, thunks are declarative chains)


A rule of thumb is to do everything as eagerly as possible

Eager operation (early return, fail fast, hoisting state, etc) clarifies whether specific dependencies exist concurrently, and makes for easy decisions

Eager operation falls in line with “separation of concerns” and a simple theme for modular, declarative, stateful, pragmatic, legible code: declare dependencies, operate, and return

Everything else is a matter of sequencing, which should be done as eagerly as possible

A reducer is the last place you’d want to be “doing” anything. It’s effectively a “return” and should simply select data from action objects

4 users liked answer #2dislike answer #24
neaumusic profile pic
neaumusic

Consider what you might want to do in your middleware...if you put all your logic inside your reducer then the record in the middleware will not have the business logic applied to it.

  1. dispatch(new UpdatePerson(person))
  2. run middleware and intercept the UpdatePerson action
  3. reducer - updates the age value for person

if you want to say, save your record in a store, then the reducer logic will run too late and you won't have access to the changed record in the state you require it in your middleware.

2 users liked answer #3dislike answer #32
atreeon profile pic
atreeon

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).