在Redux中,事件和动作是否具有一对一的关系?

34
事件(DOM事件或系统事件)是否与操作具有1:1的关系?即单击事件应该仅触发一个操作吗?
例如,假设我们有一个页面,显示一个包含10行和2列的表格。每行都有一个产品字段和一个金额字段。金额字段具有一个范围输入,范围为[0, 10]。用户可以单独设置每个产品的金额。
用户还通过使用两个按钮获得了2个选项。
按下第二个按钮将禁用表格中除第一个产品以外的所有产品(有效地将它们的金额设置为0,用户不能再与它们交互以设置它们的金额)。让我们称之为“选项B”。
按下第一个按钮将启用第一个产品后的所有产品(默认情况下为每个产品设置其金额为1),用户可以再次与它们交互,单独设置它们的金额。让我们称之为“选项A”。
此“应用程序”的状态由以下简单对象描述:
state = {
    option : <String>,
    products : [
        {
            name : <String>,
            amount : <Integer>
        }, ...
    ]
}

我们还有这4个简单的action创建者:

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 }
    }
}

为了简单起见,我们只有一个 reducer。

在这个例子中,选择 选项 B 应该对状态产生以下影响:

  • option 更改为 B
  • 将第一个之后的每个 product 的数量设为 0

选择 选项 A 应该分别对状态产生以下影响:

  • option 更改为 A
  • 将第一个之后的每个 product 的数量设为 1

增加产品 A 的数量应该对状态产生以下影响:

  • 将产品 A 的数量增加 1

实现这些更改的正确方法是什么?

a)option 按钮的 onClick 处理程序中执行以下操作:

  • 触发 store.dispatch(setOption(option))
  • 对于第一个之后的每个产品都要触发 store.dispatch(setAmount(productName, amount))amount = 1 表示选项 A,0 表示选项 B)

b)option 按钮的 onClick 处理程序中执行以下操作:

  • 触发 store.dispatch(setOption(option))

    并且让 reducer 更改每个之后产品的 option 以及数量为指定的数量(amount = 1 表示选项 A,0 表示选项 B)

如果我们选择 a),则 reducer 中 switch (action) {}语句中的每种情况都只处理状态的一个方面,但我们必须从一个 click 事件中触发多个动作。

如果我们选择 b),则我们只需要从 click 事件中触发一个动作,但是 reducer 中 SET_OPTION 的情况不仅会更改 option,还会更改产品的 amount

2个回答

46

对于这个问题,我们需要具体情况具体分析,没有一般性的答案。

在使用 Redux 时,应该努力保持 reducers 的简单性和 action log 的含义明确性的平衡。当您能够阅读 action log 并且理解为什么会发生这种情况时,这是最好的。这就是 Redux 带来的“可预测性”方面。

当您派发单个 action 时,状态的不同部分会作出相应的改变,很容易后追溯它们的更改原因。如果您调试问题,就不会被大量的操作所淹没,并且每个变化都可以追溯到用户的某个操作。

相比之下,当您针对单个用户交互派发多个 actions 时,很难确定它们被派发的原因。它们会使 action log 混乱,如果它们被分发存在错误,log 将无法揭示潜在的原因。

一个好的经验法则是永远不要在循环中使用 dispatch。这样做非常低效,并且如上所述,掩盖了更改发生原因的真实性质。在您的特定示例中,我建议触发单个 action。

但是,这并不意味着永远触发单个 action 是最佳选择。像所有事情一样,它是一个权衡。在某些情况下,触发几个 actions 可能更方便。

例如,如果您的应用程序允许用户标记产品,则将 CREATE_TAGADD_TAG_TO_PRODUCT action 分开可能更方便,因为尽管在此场景中它们同时发生,但它们也可以分别发生,而且编写处理它们为不同 actions 的 reducers 可能更容易。只要您不滥用此模式,并且不在循环中执行此操作,就应该没问题。

尽可能将操作日志与用户交互历史保持一致。但是,如果它使得reducers实现起来棘手,请考虑将某些操作拆分为多个操作。如果可以将UI更新视为两个单独的操作,那么就应该进行拆分。不要陷入极端情况。优先考虑reducers的清晰性而非完美的日志记录,但也不要因追求reducers的清晰性而在循环中派发操作。


根据我的经验,异步操作也会导致每个用户操作多个redux-actions。例如,如果用户点击“保存”按钮,您可能首先要分派同步操作以禁用用户界面的某些部分(如“保存”按钮),然后再分派异步操作以实际远程保存对象。 - bvaughn
或者跟进这个例子,如果你有一个创建按钮负责创建资源并将其分配给用户,如果你想要符合restful的标准,它应该触发2个API请求。你会如何处理? - Jeremy D
我认为,如果以此方式使你的请求变得低效,那么采用RESTful架构是一个不好的主意。我肯定会将其设计为单一请求。无论哪种情况,都没有阻止你分发多个操作。 - Dan Abramov

3
为了补充Dan的出色回答,在采用b)方式时,您仍然可以像在a)方式中所说的那样处理状态的不同部分,方法是将根reducer拆分成较小的reducer,例如Redux文档显示。您应该通过组合reducer来拆分状态处理,而不是任意地调度其他操作。正如Dan所说,它有助于表达为什么的操作。

1
感谢您的输入。那正是我最终所做的,顺便说一句,我真的很享受这个过程;它就像一个小脑筋急转弯。 - Dimitris Karagiannis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接