DEV Community

Anton Melnyk
Anton Melnyk

Posted on

Redux actions are not setters

One of the common misconceptions and myths when working with Redux is that actions are setters to the Store.

It's tempting to just add an action like setMyPropertyValue and then inside reducer check for this action and just set the property value to action.value:

// "Setter" action
const setMyProperty = value => ({ type: "SET_SOMETHING", value });

// "Setter" in reducer
case "SET_SOMETHING":
  return {
    ...state,
    myProp: action.value
  };
Enter fullscreen mode Exit fullscreen mode

While sometimes action really can be a glorified property setter, this pattern is usually a code smell and a sign of the wrong usage of Redux.

One of the main advantages and ideas of Redux is decoupling of "what happened" from "how the state was changed". That's the reason why we actually need actions and reducers separated and one of the reasons to use Redux at all.

In the action object, we are describing what happened in the application. In the reducers, we are describing how to react to that application event. In the core of the Redux is a "one to many" relationship. One action "triggers" many reducers, each of which changes its own part of the state.

If we're doing actions that begin with "set...", we are losing the "one to many" relationship between that action and reducers. In this way, we are coupling the action to specific state property. This, in turn, can lead to other problems.

Too granular dispatching

When actions become setters, thunk action creators can become functions that dispatch multiple actions in a row to perform a "state change transaction". Dispatches become too granular and meaningless, leaking state update logic to the thunk action creators functions. For example, that's how hypothetical bad action creator that adds item in the basket could look:

export const itemAdded = item => (dispatch, getState) => {
    dispatch(addItem(item));
    dispatch(totalCostUpdate(item.price));
    dispatch(applyDiscount(getState().totalCost));
};
Enter fullscreen mode Exit fullscreen mode

Here we have a basket update logic leaked to the action dispatch itself. Clearly, we could have just a single dispatch for "ADD_ITEM" and reducers should add an item, calculate the total cost and apply the discount. Although actions listed here do not have "set" in their names, they are still acting like setters for specific properties and potentially could be removed in favor of adding this logic to reducers.

Having potentially wrong state

Every dispatch and resulted state change is independent. That means that following the example above we have 3 different state shapes that change each other in a row. Is it valid to have an item added, but the total cost not updated? In terms of application logic probably not.
Having an action creator like this opens a possibility that another part of the application accidentally dispatches the "addItem" action independently and that will leave to a state that is not valid. Catching bugs like this with Redux is easy by just following Redux DevTools state changes, but instead of catching bugs and having to remember that "when adding item we must change 3 state properties" we should have Redux to remember that for us by having those properties reacting in a reducer to a single action instead 3 of them.

Decreasing performance

On every dispatch, Redux iterates all subscriptions and runs all selector functions each subscription has (technically details on this depend on the framework you are using Redux with). Some selectors can potentially have calculations of the derived state, which can make the situation even worse if selectors are not memoized.

While JavaScript is fast enough to run hundreds of functions per milliseconds and is not usually the performance bottleneck, we don't have to waste processor power, especially considering some low-end mobile devices. A smaller amount of actions can make our subscriptions run faster.

Losing centralization

One of the goals of Redux is to have the state updated by pure functions. If actions act as setters, we stop having application logic centralized and contained in pure reducers, but instead, we have it leaked and spread across action creators or even worse - some UI components.

Increasing boilerplate code

Each action involves some degree of "boilerplate" code. Especially in TypeScript, we usually need to define:

  • action type string constant via enum
  • type of the action object
  • action creator function
  • handle new action in reducer

This adds more auxiliary lines of code, but it has a purpose for real actions - strict typing, organization of code, and better visibility of how the application state can be changed. Ideally, opening the file with declared action types constants should give the developer an idea of what can possibly happen in the application. This also helps to onboard new developers on the project and work on it independently in large teams.

When having meaningful actions, we mostly have to add them when starting the project; later we can often reuse existing actions.

But, if we start to add granular setters for each small thing that happens inside the application and is actually part of some other big event, then action types are harder to reason about and the action log becomes polluted, changing too fast in the Redux DevTools which, again, decreases performance and makes state updates less meaningful.

Note: boilerplate can be also avoided by using Redux Toolkit (and probably most of the readers should use it for their Redux applications). But sometimes, it's not desirable (see Immer vs Ramda - two approaches towards writing Redux reducers) or the application you are working on is legacy codebase.

Conclusion

We should aim to treat actions as "events" in Redux applications and make sure we perform updates in reducers. Thunk actions should not be abused and have too many dispatches in a row that act as a single "transaction".

Most of the issues mentioned above are basically a reason why we are using Redux. We use it to have centralized complex state updates that are easy to reason about and work with within large teams. Actions that act as setters work against that goal.

Oldest comments (0)