在 redux 中使用样板动作和 reducer
Posted
技术标签:
【中文标题】在 redux 中使用样板动作和 reducer【英文标题】:Use of boilerplate actions and reducers in redux 【发布时间】:2018-05-27 22:30:54 【问题描述】:我一直遵循学习 React 开发的广泛建议,首先掌握组件 props
,将 UI 状态封装在组件级别 this.state
并通过组件树有选择地向下传递。这是一次启发性的经历。我开始欣赏无状态视图设计模式的强大功能,并且觉得我已经能够使用这些技术实现稳健且组织良好的结果。
继续前进,我现在尝试使用redux
整合更复杂的状态管理。但是当我涉足复杂性并将redux
集成到我的应用程序中时,我发现自己面临以下关于我的代码如何演变的观察。其中一些发展似乎是明智的,但另一些让我怀疑我做的事情是否“正确”。
1) Action Creators 作为业务和 UI 逻辑的纽带
我发现以前在 React 生命周期函数 componentDidUpdate
等和 onTouch/onPress
处理程序中实现的大部分逻辑现在都在 action creators 中实现了。这似乎是一个积极的发展,因为它将“所有东西都放在同一个地方”并允许进行单元测试。
问题:将业务逻辑集中在一个由相当复杂的动作创建者组成的网络中是否是最佳实践?
2) 挖空reducers
作为上述#1 的推论,我发现我的reducers
和它们对应的action
对象已经演变成一个事实上的设置器列表,它们只不过是使用传递的值更新状态存储,以这种方式:
case types.SAVE_ORDER:
return Object.assign(, state,
order: action.order,
);
其中很大一部分原因是reducers
应该是纯函数,因此我可以用它们做的事情受到限制(例如,没有异步处理)。此外,reducer 只允许在它们各自的存储状态子部分上运行。鉴于我的应用程序的大部分复杂性已经必然存在于 action creators 中,我发现很难证明仅仅为了让它们“看起来有用”而将复杂性迁移到 reducers
是合理的。
问题:有样板文件reducers
仅作为 redux 存储状态的美化设置器,这是否正常且可接受的做法?
3) redux-thunk
无处不在
我已经单独询问了为什么redux-thunk
甚至是必要的(而不是在异步回调/实用程序函数中调用标准操作创建者)。我被 Dan Abramov 指出了这个answer,它提供了一个非常令人满意的解释(相对于可扩展性、服务器端渲染和无数其他原因)。
接受redux-thunk
的必要性后,我发现我的大多数动作创建者需要执行异步动作,需要访问getState
,或dispatch
多次更改状态.结果,我一直在广泛地返回“thunks
”。
问题:Redux 应用程序广泛依赖thunk
'ed 动作创建者,并且很少直接触发标准对象动作是否正常?
4) Redux 作为全局 this.state
归根结底,我的应用程序的redux
商店似乎已经演变为有效地类似于全球this.state
。您可以将其视为将整个应用程序状态保留在最外层容器组件中的 this.state
中,但没有将所述 state
向下传递到 props
的嵌套层所带来的不可避免的混乱,并且任何更改都会备份组件树通过 handler 函数的老鼠巢。
问题:redux 是用于全局状态存储的正确工具吗?是否有替代品的行为更类似于 react 的内置 this.state
,允许通过无状态的 react 组件传播全局应用程序状态,并通过集中的 ' 从整个应用程序更新switchboard',没有采用 redux 带来的看似无穷无尽的样板、常量和 switch
语句网络?
5) 一种单一的动作类型? 此后续问题的灵感来自已发布的 cmets 之一。
问题:一个人是否可以合法地(严肃地说,不仅仅是公然证明一个观点)将 redux 与一种操作类型一起使用?
示例 - 动作创建者:
export function someActionCreator(various_params)
return (dispatch, getState =>
// ... business logic here ....
asyncIfThisIfThat().then(val =>
dispatch(
// type: 'UPDATE_STATE', // Don't even bother setting a type
order: val
)
)
)
通用减速机案例:
export default function app(state = initialState, action = )
return Object.assign(, state, action)
// Just unconditionally merge into state!
在我看来,这将提供一个全局范围的状态对象,该对象自动映射到connect
ed 组件,并且受益于不可变状态的所有优点,并且可以与 React props
互操作。在这个方案中,dispatch
实际上变成了一个全局的setState
。
注意 - 请不要误会这个问题 - 这当然不是对 redux 的批评。作为一名学习者,我显然无法评判一项由数千人的专业知识和数百万人支持的技术。我毫不怀疑它在正确的上下文中的价值。
我只是在自己的代码中感觉到了可疑模式的味道,想知道我做错了什么,或者我是否使用了正确的工具来完成任务。
【问题讨论】:
你不需要 redux 样板。您可以使用单个减速器进行单个操作,它仍然可以工作。您添加多少样板完全取决于您,以及您想要的详细程度。 @markerikson 有一个 redux 博客和大量资源,可以回答您的所有其他问题 github.com/markerikson/react-redux-links 【参考方案1】:尝试使用我的库redux-light。您需要使用 store.setState 和 store.getState 类似于 React.Component.setState,完全没有减速器,也没有其他样板。
【讨论】:
【参考方案2】:我的回答主要是根据我自己学习 redux 和专业使用它的经验说的。我所在的团队走上了类似 setter 动作的相同道路,然后转向更基于事件的动作名称,并描述发生了什么而不是应该发生什么。
问题:将业务逻辑集中在相当复杂的动作创建者网络中是最佳实践吗?
这取决于您的操作是如何命名的。在您的情况下,您的操作是非常荣耀的设置器,因此您的所有业务逻辑都将存在于 Action Creators 中。如果您将您的操作命名为更像事件(描述发生了什么)而不是设置器,那么您将把一些业务逻辑转移到减速器中,并从操作创建器中移除复杂性,因为事件动作在不同的 reducer 中自然感觉更可重用。当您执行 setter 操作时,倾向于使用仅与 1 个 reducer 交互的 setter-action,并在您希望其他 reducer 参与时创建更多 setter-action。
如果您有适用于学校的应用,但学生被开除,您可能会发送 REMOVE_STUDENT
和 DELETE_GRADES_FOR_STUDENT
操作。如果您的操作具有类似事件的名称,您可能更倾向于只使用一个 STUDENT_EXPELLED
操作,让成绩缩减器和学生名册缩减器都对其进行操作。
不过,从技术上讲,没有什么可以阻止您使用类似 setter 的名称,并在多个 reducer 中对它们进行操作。这不是我的团队在使用 Redux 工作并使用类似 setter 的名称时所陷入的趋势。我们不想混淆简洁的动作名称所带来的期望和纯洁性,这对状态的影响非常明显。 REMOVE_STUDENT_GRADES
和 DELETE_STUDENT_FROM_ROSTER
感觉很好。
问题:让样板化的 reducer 仅仅充当 redux 存储状态的美化设置器,这是正常且可接受的做法吗?
这是正常的,但不一定正确。这就是我们的代码库最初的增长方式——我们甚至有标准将我们的操作命名为RESET_...
、SET_...
、REMOVE_...
、ADD_...
、UPDATE...
等。这似乎工作了一段时间,直到我们碰到我们需要多个 reducer 根据单个操作进行更新的情况。
您的操作将以这两种方式之一(或两者)发展
连续调度多个动作(你必须使用像redux-batch-actions这样的库,如果你想连续调度多个动作)。我们选择不使用它,因为它很麻烦,而且随着我们的代码库规模的扩大,感觉它的扩展性不是很好。
重命名您的操作,使其更通用,并可在不同的 reducer 中重复使用。这就是我们最终要做的。将 action 作为 setter 和 getter 很麻烦。 Dan Abramov 和其他人表达了他们的观点,Redux Actions 应该是events
(对已经发生的事情的描述),而不是instructions
(对应该发生的事情的描述)。我所在的团队同意这一点,我们已经摆脱了二传手式的行动。在 Redux 刚推出的时候,这方面有很多争论。
在场景 1 中,您可能会执行以下操作:
// student has been expelled from school, remove all of their data
store.dispatch(batchActions(
removeStudentFromClass(student),
removeStudentsGrades(student)
));
// student roster reducer
case REMOVE_STUDENT_FROM_CALLS:
/* ... */
// student grades reducer
case REMOVE_STUDENT_GRADES:
/* ... */
如果您在不使用批处理操作的情况下走这条路,那绝对是一场噩梦。每个分派的事件都会重新计算状态,并重新渲染您的应用程序。这很快就崩溃了。
// student has been expelled from school, remove all of their data
store.dispatch(removeStudentFromClass(student));
// app re-rendered, students grades exist, but no student!
store.dispatch(removeStudentsGrades(student));
在上面的代码中,您发送了一个操作以将学生从课堂中移除,然后应用程序重新呈现。如果你打开了一个成绩页面,你可以看到学生的成绩,但是学生被删除了,你很可能会参考学生名册缩减器中的状态来获取学生信息,这可能/将会引发 JS 错误.坏消息。你有undefined
的学生的成绩?!我自己也遇到过这样的问题,这是我们转向下面选项 2 的动机的一部分。你会听说这类被称为“中间状态”的状态,它们是有问题的。
在场景 2中,您的代码可能看起来更像这样:
store.dispatch(expelStudent(student));
// student roster reducer
case EXPEL_STUDENT:
/* ... */
// student grades reducer
case EXPEL_STUDENT:
/* ... */
使用上面的代码,学生通过操作被开除,并且他们的数据在 1 步中从所有 reducer 中删除。这可以很好地扩展,您的代码库反映了与您的应用程序相关的业务术语,您将日常谈论这些术语。如果从业务逻辑的角度来看,您还可以对多个事件执行相同的状态更新:
case EXPEL_STUDENT:
case STUDENT_DROPPED_OUT:
case STUDENT_TRANSFERRED:
/* remove student info, all actions must have action.payload.student */
问题:redux 应用程序广泛依赖 thunk'ed 动作创建者并且很少直接触发标准对象动作是否正常?
是的,当然。一旦您需要从动作创建器的存储中获取一小段数据,它就必须变成一个 thunk。 Thunks 很常见,应该是 redux 库的一部分。
随着我们的 thunk 变得越来越复杂,它们变得混乱且难以理解。我们开始滥用承诺和返回值,这很费力。测试它们也是一场噩梦。你必须嘲笑一切,这很痛苦。
为了解决这个问题,我们引入了redux-saga。 Redux-saga 可以使用 redux-saga-test-plan 或 redux-saga-test-engine 等库轻松测试(我们使用测试引擎并根据需要对其做出贡献)。
我们不是 100% 的传奇,也不打算成为。我们仍然根据需要使用 thunk。如果您需要将动作升级为更智能一点,并且代码非常简单,那么您没有理由不将该动作升级为 thunk。
一旦动作创建者变得足够复杂,需要进行单元测试,redux-saga 就可能派上用场。
Redux-saga 确实有一个粗略的学习曲线,一开始感觉很奇怪。手动测试 sagas 很痛苦。很棒的学习经历,但我不会再这样做了。
问题:redux 是用于全局状态存储的正确工具吗?是否有替代方案的行为更类似于 react 的内置 this.state,允许全局应用程序状态通过无状态反应组件传播,并通过集中式“交换机”从整个应用程序更新,而无需看似无穷无尽的网络采用 redux 时的样板、常量和 switch 语句?
MobX - 我从那些对 Redux 有同样抱怨的人那里听到了一些关于它的好消息(太多样板,太多文件,一切都断开连接)我自己不使用它,也没有使用它,尽管。很有可能你会比 Redux 更喜欢它。它解决了同样的问题,所以如果你真的更喜欢它,那么它可能值得转换。如果您要长期从事代码工作,开发人员的经验非常重要。
我对 Redux 样板和诸如此类的东西没意见。我工作的团队制作了宏来搭建创建新动作的样板,并且我们进行了大量测试,因此我们的 Redux 代码非常可靠。一旦你使用它一段时间,你就会内化样板文件并且它不会那么令人筋疲力尽。
如果您确实长期坚持使用 Redux,并且足够精明地采用 flow on top of redux,那么对于长期可维护性来说,这是一个巨大的胜利。完全类型化的 redux 代码非常好用,尤其是重构。重构 reducer/actionCreators 非常容易,但忘记更新单元测试代码。如果你的单元测试被流程覆盖,它会抱怨你立即错误地调用了一个函数。太棒了。
引入 Flow 是一个需要克服的复杂障碍,但非常值得。我没有参与最初的设置,我认为引入代码库变得更容易,但我想这需要一些学习和几个小时。不过值得。绝对 100% 值得。
问题:是否可以合法地(严肃地说,不仅仅是公然证明一个观点)将 redux 与一个 reducer 一起使用?
您当然可以,它可以用于小型应用程序。对于更大的团队来说,它不能很好地扩展,重构似乎会成为一场噩梦。将 store 拆分为单独的 reducer 可以让您隔离职责和关注点。
【讨论】:
感谢您提供详细且写得好的帐户。我看到你的经历和我现在发现的有很多相似之处。这需要一些时间来消化,但 mobX 听起来更像是我在达到组件级别setState
的限制后寻找 redux 时的想法。对于我们当前的项目来说为时已晚,但我一定会阅读它。【参考方案3】:
我是一名 Redux 维护者。我会给你一些初步的答案,并为你指出一些学习资源,我可以根据需要回答进一步的问题。
首先,Cory Danielson 给出了一些极好的建议,我想附和他所说的几乎所有内容。
Action 创建者、reducers 和业务逻辑:
我会引用the Redux FAQ entry on splitting business logic between action creators and reducers:
对于 reducer 或 action creator 中应该包含哪些逻辑,没有一个明确的答案。一些开发人员更喜欢拥有“胖”的动作创建者,而“瘦”的减速器只是简单地将数据放入动作中并盲目地将其合并到相应的状态中。其他人试图强调保持动作尽可能小,并尽量减少 getState() 在动作创建者中的使用。 (就这个问题而言,其他异步方法,例如 sagas 和 observables 属于“动作创建者”类别。)
在你的 reducer 中加入更多的逻辑有一些潜在的好处。操作类型可能更语义化和更有意义(例如“USER_UPDATED”而不是“SET_STATE”)。此外,reducer 中的逻辑越多,意味着更多的功能会受到时间旅行调试的影响。
这条评论很好地总结了二分法:
现在,问题是在动作创建器中放什么,在减速器中放什么,在胖和瘦动作对象之间进行选择。如果你把所有的逻辑都放在动作创建器中,你最终会得到基本上声明状态更新的胖动作对象。 Reducers 变得纯粹、愚蠢、添加这个、删除那个、更新这些功能。他们将很容易组成。但是你的业务逻辑并不多。如果你在 reducer 中加入更多逻辑,你最终会得到漂亮、精简的 action 对象,大部分数据逻辑都在一个地方,但是你的 reducer 更难组合,因为你可能需要来自其他分支的信息。你最终会得到大型 reducer 或从 state 更高层获取额外参数的 reducer。
我在今年早些时候的帖子The Tao of Redux, Part 2 - Practice and Philosophy 中进一步讨论了这个话题(特别是action semantics 和thick vs thin reducers 的部分,以及a recent Reddit comment thread 的部分。
thunk 的使用:
是的,对于需要存在于组件之外的任何复杂同步逻辑(包括任何需要访问当前存储状态的代码)来说,thunk 都是一种有价值的工具。它们对于简单的异步逻辑也很有用(比如只有成功/失败处理程序的基本 AJAX 调用)。我在Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability 的帖子中讨论了 thunk 的优缺点。
在我自己的应用程序中,我在很多地方都使用了 thunk,以及 sagas 用于更复杂的异步逻辑和工作流,我强烈推荐 thunk 作为一个有用的工具。
Redux 作为全球商店
我经常说,您可以根据需要在 Redux 之上使用尽可能多或尽可能少的抽象。 You don't have to use switch statements,您可以在 reducer 中使用查找表或任何其他条件逻辑,您就是 highly encouraged to reuse reducer logic。其实还有dozens of existing utilities to generate reusable action creators and reducers,还有很多写在Redux之上的更高层次的抽象库。
使用单个盲板减速器
这是another topic I looked at in The Tao of Redux, Part 2 和someone else had a good comment another recent Reddit thread。这样做在技术上当然是可行的,但根据 Reddit 的评论,你的减速器实际上不再“拥有”状态形状。相反,动作创建者会这样做,并且没有什么能阻止他们输入对给定动作类型或减速器没有意义的数据。
正如我在The Tao of Redux, Part 1 - Implementation and Intent 中谈到的,创建 Redux 背后的关键意图之一是您应该能够查看已调度操作的日志并了解您的状态在何处/何时/为什么/如何更新。虽然 Redux 本身并不关心 action.type
字段实际包含的内容,但如果调度的操作被有意义地命名,那么操作历史日志对您(或其他开发人员)会更有意义。连续看到 10 个 "SET_STATE"
操作并不能告诉您正在发生的事情没有任何用处,虽然您可以查看每个操作的内容和产生的差异,但像 "EXPEL_STUDENT"
这样的操作类型意味着更多只是通过阅读它。此外,还可以将独特的操作类型追溯到它们在代码库中特定位置的使用位置,从而帮助您隔离数据的来源。
希望这有助于回答您的一些问题。如果您想进一步讨论,我通常会在美国时间Reactiflux chat channels on Discord 晚上闲逛。很高兴有时间过来聊天!
【讨论】:
谢谢,非常感谢您抽出宝贵时间分享您的想法。我确实看到了关于瘦/减脂剂的常见问题解答条目,但当时我不知道该怎么做。 (无主见的建议的错!)。不幸的是,许多'TodoList' 类型的例子并没有很好地营销redux
的价值。我一定会仔细研究你的博客。我对你所说的“盲人”方法很感兴趣,我将尝试发送化妆品type
s,它提供了一个没有实际副作用的日志跟踪。这可以很好地满足我的短期需求。以上是关于在 redux 中使用样板动作和 reducer的主要内容,如果未能解决你的问题,请参考以下文章
React Redux Falcor 和 RethinkDB 样板