Do events and actions have a 1:1 relationship in R

2019-03-08 14:04发布

问题:

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.

回答1:

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.



回答2:

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.