在 redux 中使用样板操作和减速器 [英] Use of boilerplate actions and reducers in redux

查看:29
本文介绍了在 redux 中使用样板操作和减速器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直遵循广泛给出的学习 React 开发的建议,首先掌握组件 props,将 UI 状态封装在组件级别 this.state 并有选择地通过组件树.这是一次有启发性的经历.我已经开始欣赏无状态视图设计模式的强大功能,并且我觉得我已经能够使用这些技术实现稳健且组织良好的结果.

继续前进,我现在正在尝试使用 redux 合并更复杂的状态管理.但是当我涉足复杂性并将 redux 集成到我的应用程序中时,我发现自己面临以下关于我的代码如何演变的观察.其中一些发展似乎是明智的,但其他发展让我怀疑我是否在做正确"的事情.

1) Action Creator 作为业务和 UI 逻辑的纽带

我发现以前在 React 生命周期函数 componentDidUpdate 等和 onTouch/onPress 处理程序中实现的大部分逻辑现在在 动作创作者.这似乎是一个积极的发展,因为它使一切都在同一个地方"并允许进行单元测试.

问题:将业务逻辑集中在由相当复杂的动作创建者组成的网络中是最佳做法吗?

2) 镂空reducers

作为上述#1 的推论,我发现我的 reducers 和它们对应的 action 对象已经演变成事实上的 setter 列表,它们的作用仅是以这种方式使用传递的值更新状态存储:

case types.SAVE_ORDER:返回 Object.assign({}, state, {订单:action.order,});

造成这种情况的很大一部分原因是 reducers 应该是纯函数,因此我可以用它们做什么(例如没有异步处理).此外,reducer 只能在它们各自的 store 状态子部分上运行.鉴于我的应用程序的大部分复杂性已经必然存在于 action creators 中,我发现很难证明仅仅为了让它们看起来有用"而任意将复杂性迁移到 reducers'.

问题:让样板reducers 仅仅作为redux store state 的美化setter 是正常的和可接受的做法吗?

3) redux-thunk 无处不在

我已经单独询问过为什么 redux-thunk 甚至是必要的(而不是在异步回调/实用函数内部调用标准动作创建者).我已经被 Dan Abramov 指出了这个 answer ,它提供了一个非常令人满意的解释(相对于可扩展性,服务器侧面渲染和无数其他原因).

接受了redux-thunk的必要性,我发现我的大多数动作创建者需要执行异步动作,需要访问getStatedispatch 对状态的多个更改.因此,我一直在广泛地返回thunks".

问题: redux 应用程序广泛依赖 thunk 的动作创建者,很少直接触发标准对象动作,这是否正常?

4) Redux 作为全局 this.state

归根结底,我的应用程序的 redux 存储似乎已经演变为有效地类似于全局 this.state.您可以将其视为将整个应用程序状态保存在最外层容器组件中的 this.state 中,但不会因为将所述 state 向下传递嵌套而不可避免地造成混乱props 层,任何更改都通过 handler 函数的老鼠巢备份组件树.

问题:redux 是用于全局状态存储的正确工具吗?是否有替代品的行为更类似于 react 的内置 this.state,允许通过无状态的 React 组件传播全局应用程序状态,并从通过一个集中的交换机"在整个应用程序中,没有采用 redux 带来的看似无穷无尽的样板、常量和 switch 语句的网络?

5) 一种单一的操作类型?这个后续问题的灵感来自于发布的评论之一.

问题:一个人是否可以合法地(严肃地说,不仅仅是公然证明一个观点)使用 redux 和精确的一种操作类型?

示例 - 动作创建者:

导出函数 someActionCreator(various_params){返回(调度,getState => {//... 这里的业务逻辑 ....asyncIfThisIfThat().then(val => {派遣({//type: 'UPDATE_STATE',//甚至不用设置类型顺序:val})}))}

通用减速机案例:

导出默认函数 app(state = initialState, action = {}) {return Object.assign({}, state, action)//只是无条件地合并到状态!}

在我看来,这将提供一个全局范围的状态对象,该对象自动映射到 connected 组件,并且受益于不可变状态的所有优点并且可与 React props 互操作.在这个方案中,dispatch 有效地变成了一个全局的 setState.

<小时>

注意 - 请不要误解这个问题 - 这当然不是对 redux 的批评.作为一名学习者,我显然无法判断一项由数千人的专业知识和数百万人的支持支持的技术.我毫不怀疑它在正确的上下文中的价值.

我只是在我自己的代码中感觉到有问题的模式的味道,想知道是什么,如果我做错了什么,或者我是否使用了正确的工具来完成任务.

解决方案

我的回答主要是根据我自己学习 redux 和专业使用它的经验.我所在的团队采用了类似 setter 的动作的相同路径,然后转向更基于事件的动作名称,并描述发生了什么而不是应该发生什么.

<块引用>

问题:将业务逻辑集中在相当复杂的动作创建者网络中是最佳做法吗?

这取决于您的操作的命名方式.在您的情况下,您的操作是非常美化的 setter,因此您的所有业务逻辑都将位于 Action Creators 中.如果您将您的操作命名为更像事件(描述发生了什么)而不是 setter,您将有一些业务逻辑转移到减速器中,并且从操作创建器中移除复杂性,因为事件操作自然会在不同的减速器中更容易重用.当您执行 setter 操作时,倾向于使用仅与 1 个 reducer 交互的 setter 操作,并在您希望其他 reducer 参与时创建更多的 setter 操作.

如果您有一个学校应用,并且有学生被开除,您可能会先发送 REMOVE_STUDENT,然后再发送 DELETE_GRADES_FOR_STUDENT 操作.如果您的操作具有类似事件的名称,您可能更倾向于只使用 STUDENT_EXPELLED 操作,成绩减少器和学生名册减少器都对其进行操作.

不过,从技术上讲,没有什么能阻止您使用类似 setter 的名称,并在多个 reducer 中对它们进行操作.这不是我的团队在 Redux 中工作并使用类似 setter 的名称时陷入的趋势.我们不想混淆来自简洁的动作名称的期望和纯度,其中对状态的影响非常明显.REMOVE_STUDENT_GRADESDELETE_STUDENT_FROM_ROSTER 感觉很好.

<块引用>

问题:让样板化的 reducer 仅仅作为 redux store state 的美化 setter 是正常的、可接受的做法吗?

这是正常的,但不一定正确.这就是我们的代码库最初的增长方式 - 我们甚至有标准将我们的操作命名为 RESET_...SET_...REMOVE_...code>, ADD_..., UPDATE... 等等.这似乎工作了一段时间,直到我们遇到需要多个reducer来更新的情况一个动作.

您的行为将以这两种方式之一(或两者)演变

  1. 连续调度多个动作(你必须使用像redux-batch-actions(如果你想连续分派多个动作).我们选择不使用它,因为它很麻烦,而且随着我们的代码库规模的增长,它的扩展性也不是很好.

  2. 将您的操作重命名为更通用且可在不同的减速器中重用.这就是我们最终要做的.将操作作为 setter 和 getter 是很麻烦的.Dan Abramov 和其他人表达了他们的观点,Redux Actions 应该是 events(对已经发生的事情的描述),而不是 instructions(对应该发生的事情的描述)发生).我工作的团队同意这一点,我们已经摆脱了二传手风格的行动.早些时候,在 Redux 是新的时候对此有很多争论.

在场景 1 中,您可能会执行以下操作:

//学生被学校开除,删除所有数据store.dispatch(batchActions(removeStudentFromClass(student),removeStudentsGrades(student)));//学生名册缩减器案例 REMOVE_STUDENT_FROM_CALLS:/* ... *///学生成绩减少器案例 REMOVE_STUDENT_GRADES:/* ... */

如果您在不使用批处理操作的情况下沿着这条路走下去,那绝对是一场噩梦.每个分派的事件都会重新计算状态,并重新呈现您的应用程序.这很快就会崩溃.

//学生被学校开除,删除所有数据store.dispatch(removeStudentFromClass(student));//应用重新渲染,学生成绩存在,但没有学生!store.dispatch(removeStudentsGrades(student));

在上面的代码中,您发送了一个将学生从课堂上移除的操作,然后应用重新渲染.如果你打开了一个成绩页面,你可以看到学生的成绩,但学生被删除了,你很可能会在学生名册缩减器中引用状态来获取学生信息,这可能/会抛出一个 JS 错误.坏消息.你有undefined学生的成绩?!我自己也遇到过这样的问题,这是我们转向下面选项 2 的部分动机.您会听到这些称为中间状态"的状态,它们是有问题的.

在场景 2 中,您的代码可能看起来更像这样:

store.dispatch(expelStudent(student));//学生名册缩减器案例 EXPEL_STUDENT:/* ... *///学生成绩减少器案例 EXPEL_STUDENT:/* ... */

通过上面的代码,学生通过动作被驱逐,并且他们的数据在1步中从所有reducer中删除.这可以很好地扩展,并且您的代码库反映了与您日常谈论的应用程序相关的业务术语.如果从业务逻辑的角度来看有意义,您还可以为多个事件执行相同的状态更新:

case EXPEL_STUDENT:案例 STUDENT_DROPPED_OUT:案例 STUDENT_TRANSFERRED:/* 删除学生信息,所有动作必须有 action.payload.student */

<块引用>

问题:redux 应用程序广泛依赖于 thunk 的动作创建者,很少直接触发标准对象动作,这是否正常?

是的.一旦您需要从动作创建器中的存储中获取一小部分数据,它就必须变成一个 thunk.Thunk 很常见,应该是 redux 库的一部分.

随着我们的 thunk 变得越来越复杂,它们变得混乱且难以理解.我们开始滥用承诺和返回值,这很费力.测试它们也是一场噩梦.你必须模拟一切,这很痛苦.

为了解决这个问题,我们引入了redux-saga.Redux-saga 很容易使用 redux-saga-test-plan<等库进行测试/strong>redux-saga-test-engine(我们使用测试引擎并根据需要对其做出贡献).

我们不是 100% 的传奇,也不打算成为.我们仍然根据需要使用 thunk.如果您需要将您的操作升级为更智能一点,并且代码非常简单,那么您没有理由不将该操作升级为 thunk.

一旦动作创建者变得复杂到需要进行一些单元测试,redux-saga 可能会派上用场.

Redux-saga 确实有一个粗略的学习曲线,一开始感觉很奇怪.手动测试 sagas 很痛苦.很棒的学习经历,但我再也不会这样做了.

<块引用>

问题:redux 是用于全局状态存储的正确工具吗?是否有其他替代方案的行为更类似于 React 的内置 this.state,允许通过无状态 React 组件传播全局应用程序状态,并通过集中式交换机"从整个应用程序更新,而无需看似无穷无尽的网络采用 redux 的样板文件、常量和 switch 语句?

MobX - 我从人们那里听到了关于它的好消息谁对 Redux 有同样的抱怨(样板文件太多,文件太多,一切都已断开连接)我自己不使用它,也没有使用过它.您很有可能比 Redux 更喜欢它.它解决了同样的问题,所以如果你真的更喜欢它,那么它可能值得转换.如果您要长期从事代码工作,开发者经验非常重要.

我对 Redux 样板和诸如此类的东西没意见.我工作的团队制作了宏来构建创建新动作的样板,并且我们进行了大量测试,因此我们的 Redux 代码非常可靠.一旦你使用它一段时间,你就会内化样板,它就不会那么累了.

如果您确实长期坚持使用 Redux,并且足够精明地采用 流在 redux 之上 这是长期可维护性的巨大胜利.全类型的 redux 代码非常好用,尤其是在重构方面.重构 reducer/actionCreators 非常容易,但忘记更新单元测试代码.如果您的单元测试被流程覆盖,它会抱怨您立即错误地调用了一个函数.太棒了.

引入 Flow 是一个需要克服的复杂障碍,但非常值得.我没有参与初始设置,我认为引入代码库会变得更容易,但我认为这需要一些学习和时间.不过值得.绝对 100% 值得.

<块引用>

问题:一个人是否可以合法地(严肃地说,不仅仅是公然证明一个观点)将 redux 与一个 reducer 一起使用?

你绝对可以,它可以适用于一个小应用程序.对于更大的团队来说,它不会很好地扩展,并且重构似乎将成为一场噩梦.将 store 拆分为单独的 reducer 可以让您隔离责任和关注点.

I have been following the widely given advice of learning React development by first mastering component props, encapsulating UI state in component level this.state and passing it down selectively through the component tree. It's been an enlightening experience. I've come to appreciate the power of the stateless view design pattern and I feel that I've been able to achieve robust and well organized results using these techniques.

Moving on, I am now trying to incorporate more sophisticated state management using redux. But as I wade through the complexity and integrate redux into my apps, I find myself confronting the following observations about how my code has evolved. Some of these developments seem sensible, but others make me question whether I'm doing things 'right'.

1) Action Creators as the nexus of business and UI logic

I find that much of the logic that was previously implemented in the React lifecycle functions componentDidUpdate etc., and in onTouch/onPress handlers, is now implemented in action creators. This seems to be a positive development as it keeps 'everything in the same place' and allows for unit testing.

Question: Is it best practice to concentrate business logic in a web of fairly intricate action creators?

2) Hollowed out reducers

As a corollary to #1 above, I find that my reducers and their corresponding action objects have evolved into a de-facto list of setters that do little more than update the state store with the passed along values, in this fashion:

case types.SAVE_ORDER: 
  return Object.assign({}, state, {
    order: action.order,
  });

A big part of the reason for this is that reducers are supposed to be pure functions and therefore I'm limited in what I can do with them (e.g. no async processing). Additionally, reducers are allowed only to operate on their respective sub-section of the store state. Given that much of my app's complexity already necessarily resides in the action creators, I find it hard to justify arbitrarily migrating complexity into reducers simply for the sake of making them 'look useful'.

Question: Is it normal, and acceptable practice to have boilerplate reducers that function merely as glorified setters to the redux store state?

3) redux-thunk everywhere

I've asked separately on SO why redux-thunk is even necessary (as opposed to calling standard action creators inside of async callbacks/utility functions). I've been pointed to this answer by Dan Abramov which provides a very satisfactory explanation (vis-a-vis scalability, server side rendering and myraid other reasons).

Having accepted the necessity of redux-thunk, I find that the majority of my action creators need to perform async actions, need access to getState, or dispatch multiple changes to the state. As a result I've been returning 'thunks' extensively.

Question: Is it normal for a redux application to rely extensively on thunk'ed action creators, and rarely to fire a standard object action directly?

4) Redux as global this.state

In the final analysis, it seems my app's redux store has evolved to effectively resemble a global this.state. You could think of it as keeping the entire application state in this.state in the outermost container component, but without the inevitable mess that comes with passing the said state down through nested layers of props, and any changes back up the component tree through a rats-nest of handler functions.

Question: Is redux the correct tool to use for a global state store? Are there alternatives out there that behave more akin to react's built-in this.state, allowing a global application state to be propagated through stateless react components, and updated from throughout the application via a centralized 'switchboard', without the seemingly endless web of boilerplate, constants and switch statements that come with adopting redux?

5) One single action type? This follow up question is inspired by one of the posted comments.

Question: Could one legitimately (in all seriousness, not just blatantly demonstrating a point) use redux with precisely one action type?

Example - Action creator:

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

The one universal reducer case:

export default function app(state = initialState, action = {}) {
  return Object.assign({}, state, action)
  // Just unconditionally merge into state!
}

Seems to me this would provide a globally scoped state object that is automatically mapped to connected components, and one that benefits from all the advantages of immutable state and interoperable with React props. In this scheme, dispatch effectively becomes a global setState.


Note - Please don't take this question wrong - this is certainly not criticism of redux. As a learner I am obviously in no position to judge a technology backed by the expertise of thousands and the support of millions. I have no doubt of its value in the right context.

I'm just sensing the smell of a questionable pattern in my own code and wondering what, if anything I'm doing wrong, or whether I'm using the right tool for the task.

解决方案

My answer is mostly speaking from my own experience learning redux and using it professionally. The team I was on went down the same path of setter-like actions, and then shifted away to action names that were more event-based and describe what had happened rather than what should happen.

Question: Is it best practice to concentrate business logic in a web of fairly intricate action creators?

This depends on how your actions are named. In your case, your actions are very glorified setters, so all of your business logic is going to live inside of Action Creators. If you name your actions to be more event-like (descriptive about what happened) rather than setters, you're going to have some of the business logic shift into the reducers, and complexity removed from action creators, because event actions naturally feel more re-usable across different reducers. When you do setter actions, the tendency is to have setter-actions that interact with only 1 reducer, and create more setter-actions when you want other reducers to be involved.

If you have an app for a school, and a student is expelled, you'll likely dispatch a REMOVE_STUDENT and then aDELETE_GRADES_FOR_STUDENT action. If your action has an event-like name, you may be more inclined to just have a STUDENT_EXPELLED action that the grades reducer and student roster reducer both act upon it.

There is nothing technically stopping you from having setter-like names, and acting on them in multiple reducers, though. It's just not the tendency that my team fell into when working in Redux and using setter-like names. We didn't want to muddy up the expectations and purity that came from having concise action names, where the impact on state was very clear. REMOVE_STUDENT_GRADES and DELETE_STUDENT_FROM_ROSTER felt good.

Question: Is it normal, and acceptable practice to have boilerplate reducers that function merely as glorified setters to the redux store state?

It is normal, but not necessarily correct. This is how our codebase grew initially - we even had standards to name our actions as RESET_..., SET_..., REMOVE_..., ADD_..., UPDATE... etc. This seemed to work for a while, until we bumped into cases where we needed multiple reducers to update according to a single action.

You actions will evolve in one of these 2 ways (or both)

  1. Dispatch multiple actions in a row (you must use a library like redux-batch-actions if you want to dispatch multiple actions in a row). We chose not to use this, because it's cumbersome and didn't feel like it scaled very well as our codebase grew in size.

  2. Rename your actions to be more generic and re-usable across different reducers. This is what we ended up doing. Having actions as setters and getters was cumbersome. Dan Abramov and others has expressed their opinion that Redux Actions should be events (a description of a thing that has happened), rather than instructions (a description of a thing that should happen). The team I work on agreed with this, and we've moved away from the setters-style of actions. There was much debate about this earlier on when Redux was new.

In scenario 1, you might do something like this:

// 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:
    /* ... */

If you go down this path without using Batch Actions, it's an absolute nightmare. Each dispatched event will recompute state, and re-render your app. This falls apart very quickly.

// 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));

In the above code, you dispatch an action to remove the student from class, and then the app re-renders. If you have a grades page open, and you can see the students grades, but the student is removed, you're very likely going to reference state in the student roster reducer to grab the student info and that can/will throw a JS error. Bad news. You have the grades for a student of undefined?! I ran into issues like this myself, and it was part of our motivation for moving to option 2 below. You'll hear about these kinds of states called "intermediate states" and they're problematic.

In scenario 2 your code might look more like this:

store.dispatch(expelStudent(student));

// student roster reducer
case EXPEL_STUDENT:
    /* ... */

// student grades reducer
case EXPEL_STUDENT:
    /* ... */

With the code above, the student is expelled via the action, and their data is removed from all reducers in 1 step. This scales nicely and your codebase reflects the business terms related to your app that you would talk about day-to-day. You can also perform the same state updates for multiple events if it makes sense from a business logic perspective:

case EXPEL_STUDENT:
case STUDENT_DROPPED_OUT:
case STUDENT_TRANSFERRED:
    /* remove student info, all actions must have action.payload.student */

Question: Is it normal for a redux application to rely extensively on thunk'ed action creators, and rarely to fire a standard object action directly?

Yes definitely. As soon as you need to grab a little piece of data from the store in an action creator, it has to become a thunk. Thunks are very common, and should have been part of the redux library.

As our thunks grew in complexity, they got confusing and difficult to easily understand. We started to abuse promises and return values and it was taxing. Testing them was also a nightmare. You have to mock out everything, it's painful.

To solve this problem, we pulled in redux-saga. Redux-saga is easily testable with libraries like redux-saga-test-plan or redux-saga-test-engine (we use test-engine and have contributed to it as needed).

We aren't 100% sagas, and don't aim to be. We still use thunks as needed. If you need to upgrade your action to be a little smarter, and the code is very simple, there's no reason why you shouldn't upgrade that action to a thunk.

As soon as an action creator gets complex enough to warrant some unit testing, redux-saga might come in handy.

Redux-saga does have a rough learning curve to it, and feels quite bizarre at first. Testing sagas manually is miserable. Great learning experience, but I would not ever do it again.

Question: Is redux the correct tool to use for a global state store? Are there alternatives out there that behave more akin to react's built-in this.state, allowing a global application state to be propagated through stateless react components, and updated from throughout the application via a centralized 'switchboard', without the seemingly endless web of boilerplate, constants and switch statements that come with adopting redux?

MobX - I've heard good things about it from people who have your same complaints about Redux (too much boilerplate, too many files, everything is disconnected) I don't use it myself and have not used it, though. There's a good chance that you'll enjoy it more than Redux. It solves the same problem, so if you actually enjoy it more then it may be worth the switch. Developer experience is very important if you're going to work on code for a long time.

I'm okay with the Redux boilerplate and whatnot. The team I worked on has made macros to scaffold out the boilerplate of creating new actions, and we have lots of tests in place so our Redux code is pretty solid. Once you work with it for a while, you internalize the boilerplate and it's not as exhausting.

If you do stick with Redux long term, and are savvy enough to adopt flow on top of redux it's a huge win for long-term maintainability. Fully-typed redux code is amazing to work in, especially for refactoring. It's so easy to refactor a reducer/actionCreators, but forget to update unit test code. If your unit tests are covered by flow, it's going to complain that you're calling a function incorrectly immediately. It's wonderful.

Introducing Flow is a complex hurdle to get over, but well worth it. I wasn't involved in the initial set up, and I assume it's gotten easier to introduce to a codebase, but I'd imagine that it will take some learning and hours. Worth it though. Definitely 100% worth it.

Question: Could one legitimately (in all seriousness, not just blatantly demonstrating a point) use redux with precisely one reducer?

You definitely could, it could work for a small app. It wouldn't scale well for a larger team, and refactoring seems like it would become a nightmare. Splitting the store up into separate reducers lets you isolate responsibilities and concerns.

这篇关于在 redux 中使用样板操作和减速器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆