我一直在遵循广泛给出的学习React开发的建议,首先掌握组件props
,在组件级this.state
中封装UI状态,并有选择地通过组件树传递它。这是一次启发性的经历。我已经开始意识到无状态视图设计模式的强大功能,我觉得我已经能够使用这些技术获得稳健且组织良好的结果。
继续,我现在尝试使用redux
合并更复杂的状态管理。但是当我逐渐考虑复杂性并将redux
集成到我的应用程序中时,我发现自己面临着关于我的代码如何演变的以下观察。其中一些发展似乎是明智的,但其他人让我怀疑我是否正确地做事。#/ p>
1)Action Creators是业务和UI逻辑的关联
我发现以前在React生命周期函数componentDidUpdate
等以及onTouch/onPress
处理程序中实现的大部分逻辑现在都在动作创建器中实现。这似乎是一个积极的发展,因为它保持在同一个地方的一切'并允许进行单元测试。
问题:将业务逻辑集中在相当复杂的动作创建者网络中是最佳做法吗?
2)挖空reducers
作为上述#1的推论,我发现我的reducers
及其相应的action
对象已演变成事实上的setter列表,只需更新状态存储区即可。以这种方式传递价值观:
case types.SAVE_ORDER:
return Object.assign({}, state, {
order: action.order,
});
其中很大一部分原因是reducers
应该是纯函数,因此我可以用它来做限制(例如没有异步)处理)。另外,减速器仅允许在它们各自的商店状态的子部分上操作。鉴于我的应用程序的大部分复杂性已经必然存在于动作创建者中,我发现很难证明任意将复杂性迁移到reducers
只是为了制作它们&# 39;看起来很有用'。
问题:将样板reducers
仅仅作为减少存储状态的美化设置器的样板redux-thunk
是否正常且可接受的做法?
3)redux-thunk
无处不在
我已经单独询问了为什么redux-thunk
甚至是必要的(而不是在异步回调/效用函数中调用标准动作创建者)。我已经被Dan Abramov指出这个answer,它提供了一个非常令人满意的解释(相对于可扩展性,服务器端渲染和其他原因)。
接受getState
的必要性后,我发现我的行动创建者的大多数都需要执行异步操作,需要访问dispatch
或{{1}多次改变状态。因此,我一直在回归thunks
'广泛。
问题:还原应用程序广泛依赖于thunk
的动作创建者,并且很少直接触发标准对象操作,这是正常的吗?
4)Redux为全球this.state
归根结底,我的应用程序的redux
商店似乎已经发展到有效地类似于全球this.state
。您可以将其视为将最整个应用程序状态保留在最外层容器组件中的this.state
中,但没有将所述state
向下传递到props
的嵌套层所带来的不可避免的混乱,并且任何更改都会通过处理程序函数的鼠窝来备份组件树。
问题 redux 是否是用于全局状态商店的正确工具?是否存在类似于 react 的内置this.state
的替代方案,允许通过无状态响应组件传播全局应用程序状态,并从整个通过集中式交换机应用程序,没有看似无穷无尽的样板文件,常量和采用 redux 的switch
语句?
5)单一动作类型? 此后续问题的灵感来自其中一条发布的评论。
问题:合法地(严肃地说,不仅仅是公然展示一个观点)合法地使用带有一种动作类型的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 的批评。作为一个学习者,我显然无法判断由数千人的专业知识和数百万人的支持所支持的技术。我毫不怀疑它在正确的背景下的价值。
我只是在自己的代码中感受到一种可疑模式的气味,并想知道我做错了什么,或者我是否使用正确的工具来完成任务。< / p>
答案 0 :(得分:6)
我的回答主要是根据我自己的经验来学习redux并专业地使用它。我所在的团队采取了类似二传手的行动的方式,然后转移到更基于事件的行动名称,并描述发生的事情,而不是应该发生的事情。
问题:将业务逻辑集中在一个相当复杂的动作创建者网络中是最佳做法吗?
这取决于您的操作的命名方式。在您的情况下,您的行为是非常美化的制定者,因此您的所有业务逻辑都将存在于Action Creators中。如果您将操作命名为更像事件(描述发生的事情)而不是设置者,那么您将会将某些业务逻辑转移到reducers中,并且复杂性从动作创建者,因为事件动作自然会在不同的减速器中更易于重复使用。当你做setter动作时,倾向于让setter-actions只与1个reducer交互,并在你想要其他reducer参与时创建更多的setter-actions。
如果您有一个学校的应用程序,并且学生被开除,您可能会发送REMOVE_STUDENT
然后DELETE_GRADES_FOR_STUDENT
行动。如果您的行为具有类似事件的名称,您可能更倾向于采取STUDENT_EXPELLED
行动,等级减速器和学生名单减速器都会对其起作用。
从技术上讲,没有什么可以阻止你拥有类似二传的名字,并且在多个减速器中对它们采取行动。这并不是我的团队在使用Redux工作并使用类似setter的名字时陷入的倾向。我们并不想让简明的行动名称产生的期望和纯度变得混乱,因为这些名称对国家的影响非常明显。 REMOVE_STUDENT_GRADES
和DELETE_STUDENT_FROM_ROSTER
感觉良好。
问题:使用样板减速器只是作为减少存储状态的美化设置者,这是正常的,可接受的做法吗?
这是正常的,但不一定正确。这就是我们的代码库最初发展的方式 - 我们甚至有标准将我们的行为命名为RESET_...
,SET_...
,REMOVE_...
,ADD_...
,UPDATE...
等。工作一段时间,直到我们遇到需要多个减速器才能根据一个动作进行更新的情况。
您的行为将以这两种方式之一(或两种方式)发展
连续发送多个操作(如果要连续调度多个操作,必须使用redux-batch-actions之类的库)。我们选择不使用它,因为它很麻烦,并且随着我们的代码库规模不断扩大而感觉它的扩展性非常好。
将您的操作重命名为更通用,并可在不同的reducer中重复使用。这就是我们最终做的事情。作为制定者和吸气者采取行动是麻烦的。 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的动机的一部分。你会听到这些称为&#34;中间状态的状态&#34;他们有问题。
在方案2中,您的代码可能看起来更像这样:
store.dispatch(expelStudent(student));
// student roster reducer
case EXPEL_STUDENT:
/* ... */
// student grades reducer
case EXPEL_STUDENT:
/* ... */
使用上面的代码,学生将通过操作被驱逐,并且他们的数据将在一步中从所有Reducer中删除。这可以很好地扩展,您的代码库反映了与您日常谈论的应用相关的业务术语。如果从业务逻辑角度来看,您还可以为多个事件执行相同的状态更新:
case EXPEL_STUDENT:
case STUDENT_DROPPED_OUT:
case STUDENT_TRANSFERRED:
/* remove student info, all actions must have action.payload.student */
问题:redux应用程序是否正常依赖于thunk行为创建者,并且很少直接触发标准对象操作?
肯定是的。一旦你需要从动作创建者的商店中获取一小部分数据,就必须成为一个thunk。 Thunk很常见,应该是redux库的一部分。
随着我们的复杂性增加,他们感到困惑,难以理解。我们开始滥用承诺和回报价值,这很费劲。测试它们也是一场噩梦。你必须嘲笑一切,这很痛苦。
为了解决这个问题,我们进入了redux-saga。使用redux-saga-test-plan或redux-saga-test-engine之类的库可以轻松测试Redux-saga(我们使用测试引擎并根据需要为其做出贡献)。
我们不是100%的传奇,并且不打算成为。我们仍然根据需要使用thunk。如果您需要将操作升级为更智能,并且代码非常简单,那么您就没有理由不将该操作升级为thunk。
一旦动作创建者变得足够复杂以保证某些单元测试,redux-saga可能会派上用场。
Redux-saga确实有一个粗略的学习曲线,起初感觉非常奇怪。手动测试传奇是痛苦的。很棒的学习经历,但我不会再这样做了。
问题:redux是否是用于全球状态商店的正确工具?是否存在类似于内置this.state的反应的替代方案,允许全局应用程序状态通过无状态反应组件传播,并通过集中式交换机从整个应用程序更新&# 39;,没有看似无穷无尽的样板文件,常量和转换语句,采用redux?
MobX - 我对那些对Redux有同样抱怨的人说了很多好话(太多样板,文件太多,一切都断线了)我自己也没有用过但是并没有使用它。您很有可能比Redux更享受它。它解决了同样的问题,所以如果你真的喜欢它,那么它可能值得转换。如果您长时间处理代码,开发人员的经验非常重要。
我可以使用Redux样板和诸如此类的东西。我工作的团队已经制作了宏来支持创建新操作的样板,并且我们已经进行了大量测试,因此我们的Redux代码非常可靠。一旦你使用它一段时间,你就会内化样板并且它不会让人筋疲力尽。
如果你长期坚持使用Redux,并且足够聪明地采用flow on top of redux,那么它对于长期可维护性来说是一个巨大的胜利。完全类型的redux代码非常棒,尤其适用于重构。重构reducer / actionCreators非常容易,但忘记更新单元测试代码。如果你的单元测试被流程覆盖,它会抱怨你立即错误地调用了一个函数。太棒了。
介绍Flow是一个很难克服的障碍,但非常值得。我没有参与初始设置,我认为它更容易引入代码库,但我想这需要一些学习和时间。值得一提。绝对100%值得。
问题:合法地(严肃地说,不仅仅是公然展示一点)合法地使用redux和一个减速器吗?
你绝对可以,它适用于一个小应用程序。它对于一个更大的团队来说不会很好地扩展,而重构似乎会成为一场噩梦。将商店拆分为单独的减压器可以让您分离责任和顾虑。
答案 1 :(得分:4)
我是Redux维护者。我会给你一些初步答案并指出一些学习资源,我可以根据需要回答其他问题。
首先,Cory Danielson给出了一些优秀的建议,我想回应他所说的一切。
动作创建者,缩减者和业务逻辑:
我引用the Redux FAQ entry on splitting business logic between action creators and reducers:
对于缩减器或动作创建者应该采用哪些逻辑,没有一个明确的答案。一些开发人员更喜欢拥有“胖”动作创建器,其中“瘦”缩减器只是简单地将动作中的数据接收并盲目地将其合并到相应的状态中。其他人试图强调保持尽可能小的动作,并最小化动作创建者中getState()的使用。 (出于这个问题的目的,其他异步方法,例如sagas和observables属于&#34;动作创建者&#34;类别。)
将更多逻辑放入减速机有一些潜在的好处。动作类型可能更具语义性和更有意义(例如&#34; USER_UPDATED&#34;而不是&#34; SET_STATE&#34;)。此外,在reducer中使用更多逻辑意味着更多功能将受到时间旅行调试的影响。
这个评论很好地总结了二分法:
现在,问题是在动作创建器中放置什么,以及在reducer中,fat和thin动作对象之间的选择。如果将所有逻辑放在动作创建器中,最终会生成基本上声明状态更新的胖动作对象。 Reducers变得纯粹,愚蠢,添加 - 删除它,更新这些功能。它们很容易构成。但是你的业务逻辑并不多。如果你在reducer中加入了更多的逻辑,你最终会得到漂亮的瘦动作对象,大部分数据逻辑都集中在一个地方,但是你的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,并强烈推荐thunks作为一个有用的工具。
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的评论,你的减速器实际上并不是自己的#34;国家形状了。相反,动作创造者会这样做,并且没有什么能阻止他们输入对给定动作类型或减速器没有意义的数据。
正如我在The Tao of Redux, Part 1 - Implementation and Intent中所谈到的,Redux创建背后的一个关键意图是,您应该能够查看已发送操作的日志,并了解您的状态的位置/时间/原因/方式更新。虽然Redux本身并不关心action.type
字段实际包含的内容,但如果调度的操作具有有意义的命名,则操作历史记录日志对您(或其他某些开发人员)更有意义。连续查看10个"SET_STATE"
个操作会告诉您没有任何有用的内容,而可以查看每个操作的内容以及生成的差异,操作类型像"EXPEL_STUDENT"
一样只是通过阅读它意味着更多。此外,还可以将唯一操作类型追溯到它们在代码库中的特定位置使用的位置,从而帮助您隔离数据的来源。
希望这有助于回答您的一些问题。如果您想进一步讨论事情,我通常会在美国时间的Reactiflux chat channels on Discord晚上闲逛。很高兴你有机会过来聊天!
答案 2 :(得分:1)
尝试使用我的库redux-light。您需要使用 store.setState 和 store.getState 类似于 React.Component.setState ,完全没有减速器,也没有其他样板。