Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do events and actions have a 1:1 relationship in Redux?

Do events (DOM events or system events) have a 1:1 relationship with actions? i.e. should a single click event trigger only one action?

For example, let's say we have a page which displays a table of 10 rows and 2 columns. Each row has a Product field and an Amount field. The Amount field has a range input with a range of [0, 10]. The user can set the Amount of each Product individually.

The user is also given 2 options, through the use of 2 buttons.

  • Pressing the second button will disable all but the first product in the table (effectively setting their Amount to 0 and the user can no longer interact with them to set their Amount). Let's call this Option B
  • Pressing the first button enables all Products after the first (by default setting their Amount to 1 for each of them) and the user can once again interact with them, to set their amounts individually. Let's call this Option A.
  Option A selected:      | PRODUCT          | AMOUNT    |     |------------------|-----------|     | Product A        |   - 4 +   |     | Product B        |   - 0 +   |     | Product C        |   - 4 +   |     ````````````````````````````````   _________ | Option A|  OPTION B  `````````  Option B selected:      | PRODUCT          | AMOUNT    |     |------------------|-----------|     | Product A        |   - 4 +   |     | Product B        |  Disabled | (Amount == 0)     | Product C        |  Disabled | (Amount == 0)     ````````````````````````````````            _________ OPTION A | OPTION B|           `````````  Option A selected again:      | PRODUCT          | AMOUNT    |     |------------------|-----------|     | Product A        |   - 4 +   |     | Product B        |   - 1 +   |     | Product C        |   - 1 +   |     ````````````````````````````````   _________ | Option A|  OPTION B  `````````   

The state of this 'app' is described by this simple object

state = {     option : <String>,     products : [         {             name : <String>,             amount : <Integer>         }, ...     ] } 

We also have these 4 simple action creators:

function setOption(option) {     return { type : 'SET_OPTION', option : option}; }  function incAmount(productName) {     return {         type : 'INCREMENT_AMOUNT',         product : productName     } }   function decAmount(productName) {     return {         type : 'DECREMENT_AMOUNT',         product : productName     } }  function setAmount(productName, amount) {     return {         type : 'SET_AMOUNT',         payload : { product : productName, amount : amount }     } } 

For the sake of simplicity, we have only one reducer.

In this example, selecting Option B should have the following effects on the state :

  • Change option to B
  • Set the amount of every product after the first to 0

Selecting Option A should have the following effects on the state, respectively :

  • Change option to A
  • Set the amount of every product after the first to 1

Incrementing the amount of Product A should have the following effects on the state :

  • Increment the amount of Product A by 1

What would be the proper way to implement these changes?

a) Have the onClick handler of the option buttons do the following:

  • Fire a store.dispatch(setOption(option))
  • For each product after the first one fire a store.dispatch(setAmount(productName, amount)) (amount = 1 for option A, 0 for option B)

b) Have the onClick handler of the option buttons do the following:

  • Fire a store.dispatch(setOption(option))

    And have the reducer change the option as well as the amount of every product after the first one to the specified amount (amount = 1 for option A, 0 for option B)

If we go with a) each case in the switch (action) {} statement of the reducer deals with just one aspect of the state, but we have to fire more than one action from one click event

If we go with b) we fire only one action from the click event but the case for SET_OPTION in the reducer not only changes the option but also the amount of products.

like image 369
Dimitris Karagiannis Avatar asked Feb 15 '16 10:02

Dimitris Karagiannis


People also ask

How actions and reducers are connected in Redux?

A Redux app really only has one reducer function: the "root reducer" function that you will pass to createStore later on. That one root reducer function is responsible for handling all of the actions that are dispatched, and calculating what the entire new state result should be every time.

Are Redux actions synchronous?

Introduction. By default, Redux's actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Redux also allows for middleware that sits between an action being dispatched and the action reaching the reducers.

How are actions defined in Redux?

Redux data flow. Actions: Actions are a plain JavaScript object that contains information. Actions are the only source of information for the store. Actions have a type field that tells what kind of action to perform and all other fields contain information or data.

Does Redux support multiple actions?

Well, no matter what you pass to dispatch , it is still a single action. Even if your action is an array of objects, or a function which can then create more action objects!


2 Answers

There is no general answer to this question so we have to evaluate on a case by case basis.

When using Redux, you should strive to keep a balance between keeping reducers simple and keeping the action log meaningful. It is best when you can read the action log and it makes sense why things happened. This is the “predictability” aspect that Redux brings.

When you dispatch a single action, and different parts of the state change in response, it is easy to tell why they change later. If you debug a problem, you are not overwhelmed by the amount of actions, and every mutation can be traced to something a user did.

By constrast, when you dispatch multiple actions in response to a single user interaction, it is harder to tell why they were dispatched. They clutter the action log, and if there is a mistake in how they were dispatched, the log won’t uncover the underlying reasons.

A good rule of thumb is that you never want to dispatch in a loop. This is highly inefficient and, as noted above, obscures the true nature of why the change happened. In your particular example I would recommend firing a single action.

However this does not mean that firing a single action is always the way to go. Like everything, it is a tradeoff. There are valid cases when it is more convenient to fire several actions in response to a single user interaction.

For example, if your app lets users tag products, it can be more convenient to separate CREATE_TAG and ADD_TAG_TO_PRODUCT actions because while in this scenario they happen at the same time, they may also happen separately, and it can be easier to write reducers that handle them as different actions. As long as you don’t abuse this pattern and don’t do something like this in a loop, you should be fine.

Keep action log as close to the history of user interactions as you can. However if it makes reducers tricky to implement consider splitting some actions in several, if a UI update can be thought of two separate operations that just happen to be together. Don’t fall into either of the extremes. Prefer reducer clarity to a perfect log, but also prefer not dispatching in a loop to reducer clarity.

like image 119
Dan Abramov Avatar answered Sep 18 '22 14:09

Dan Abramov


To add to Dan's excellent answer, when you go the b) way, you can still handle separate parts of the state like you said in the a) way by splitting the root reducer into smaller ones, like Redux docs show. You should split state handling by composing reducers, not by arbitrarily dispatch other actions. As Dan said, it helps actions expressing the why.

like image 25
DjebbZ Avatar answered Sep 20 '22 14:09

DjebbZ