你没有。
但。。。你应该使用redux-saga:)
Dan Abramov的答案是正确的,但我会更多地谈论一些类似但更强大的redux-saga。redux-thunk
命令式 VS 声明式
-
DOM:jQuery is imperative / React is declarative
-
Monads:IO是命令式的/Free是声明性的
-
Redux 效应:是命令式的/是声明性的
redux-thunk
redux-saga
当你手里有一个锤子,比如一个IO monad或一个承诺,你不能轻易知道一旦你执行了它会做什么。测试 thunk 的唯一方法是执行它,并嘲笑调度程序(或者如果它与更多东西交互,则嘲笑整个外部世界...
如果你使用的是模拟,那么你就不是在做函数式编程。
通过副作用的镜头来看,模拟是你的代码不纯的标志,在函数式程序员的眼中,证明有什么地方是错误的。与其下载一个图书馆来帮助我们检查冰山是否完好无损,我们应该在它周围航行。一个铁杆TDD/Java家伙曾经问我,你是如何在Clojure中嘲笑的。答案是,我们通常不会。我们通常将其视为需要重构代码的标志。
源
sagas(因为它们是在 中实现的)是声明性的,就像Free monad或React组件一样,它们更容易测试,没有任何模拟。redux-saga
另请参阅此文章:
在现代FP中,我们不应该编写程序 - 我们应该编写程序的描述,然后我们可以随意内省,转换和解释。
(实际上,Redux-saga就像一个混合体:流程是命令性的,但效果是声明性的)
混乱:操作/事件/命令...
在前端世界中,对于一些后端概念(如CQRS / EventSourcing和Flux / Redux)如何相关存在很多困惑,主要是因为在Flux中我们使用术语“action”,它有时可以表示命令性代码()和事件()。我相信,就像事件溯源一样,你应该只调度事件。LOAD_USER
USER_LOADED
在实践中使用传奇
想象一下,一个应用程序具有指向用户配置文件的链接。使用每个中间件处理此问题的惯用方法是:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
这个传奇故事翻译为:
每次单击用户名时,获取用户配置文件,然后使用加载的配置文件调度事件。
如您所见,有一些优点。redux-saga
许可证的使用表示您只对获取上次单击的用户名的数据感兴趣(在用户快速单击大量用户名的情况下处理并发问题)。这种东西很难用thunks。如果您不希望出现此行为,则可以使用。takeLatest
takeEvery
你让动作创作者保持纯洁。请注意,保留 actionCreators(在 sagas 和组件中)仍然很有用,因为它可能有助于您在将来添加操作验证(断言/流/类型脚本)。put
dispatch
您的代码变得更加可测试,因为效果是声明性的
您不再需要触发类似 rpc 的调用,例如 .你的 UI 只需要调度发生了什么。我们只触发事件(总是过去时态!),而不是行动。这意味着您可以创建解耦的“鸭子”或有界上下文,并且saga可以充当这些模块化组件之间的耦合点。actions.loadUser()
这意味着您的视图更易于管理,因为它们不再需要包含已发生的情况和应该发生的事情之间的转换层作为效果。
例如,想象一个无限滚动视图。 可以导致,但是可滚动容器是否真的有责任决定我们是否应该加载另一个页面?然后,他必须知道更复杂的事情,例如最后一页是否已成功加载,或者是否已经有一个页面尝试加载,或者是否没有更多的项目可供加载?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已经滚动。页面的加载是该滚动的“业务效果”CONTAINER_SCROLLED
NEXT_PAGE_LOADED
有些人可能会争辩说,生成器本身可以使用局部变量将状态隐藏在redux存储之外,但是如果您通过启动计时器等开始在thunks内部编排复杂事物,那么无论如何您都会遇到同样的问题。现在有一种效果允许从 Redux 商店中获取一些状态。select
Sagas可以进行时间旅行,还可以实现目前正在开发的复杂流日志记录和开发工具。以下是一些已实现的简单异步流日志记录:
解耦
Sagas不仅取代了redux thunks。它们来自后端/分布式系统/事件采购。
一个非常普遍的误解是,sagas只是为了用更好的可测试性取代你的redux thunks。实际上,这只是 redux-saga 的一个实现细节。使用声明性效果比 thunks 更好,但 saga 模式可以在命令式或声明性代码之上实现。
首先,saga是一个软件,允许协调长时间运行的事务(最终一致性)和跨不同边界上下文的事务(域驱动的设计术语)。
为了简化前端世界的这一点,想象一下有widget1和widget2。当单击 widget1 上的某些按钮时,它应该会对 widget2 产生影响。widget1 不是将 2 个小部件耦合在一起(即 widget1 调度一个以 widget2 为目标的操作),widget1 只调度其按钮被单击。然后,saga 侦听此按钮单击,然后通过分离 widget2 知道的新事件来更新 widget2。
这增加了一个对于简单应用不必要的间接寻址级别,但可以更轻松地缩放复杂应用程序。现在,您可以将 widget1 和 widget2 发布到不同的 npm 存储库,这样它们就不必相互了解,而不必让它们共享全局操作注册表。这 2 个小部件现在是可以单独存在的有界上下文。它们不需要彼此保持一致,也可以在其他应用程序中重复使用。saga是两个小部件之间的耦合点,它们以有意义的方式协调它们,为您的企业服务。
关于如何构建 Redux 应用的一些不错的文章,出于解耦原因,您可以使用 Redux-saga:
A concrete usecase: notification system
I want my components to be able to trigger the display of in-app notifications. But I don't want my components to be highly coupled to the notification system that has its own business rules (max 3 notifications displayed at the same time, notification queueing, 4 seconds display-time etc...).
I don't want my JSX components to decide when a notification will show/hide. I just give it the ability to request a notification, and leave the complex rules inside the saga. This kind of stuff is quite hard to implement with thunks or promises.
I've described here how this can be done with saga
Why is it called a Saga?
The term saga comes from the backend world. I initially introduced Yassine (the author of Redux-saga) to that term in a long discussion.
Initially, that term was introduced with a paper, the saga pattern was supposed to be used to handle eventual consistency in distributed transactions, but its usage has been extended to a broader definition by backend developers so that it now also covers the "process manager" pattern (somehow the original saga pattern is a specialized form of process manager).
Today, the term "saga" is confusing as it can describe 2 different things. As it is used in redux-saga, it does not describe a way to handle distributed transactions but rather a way to coordinate actions in your app. could also have been called . redux-saga
redux-process-manager
See also:
Alternatives
If you don't like the idea of using generators but you are interested by the saga pattern and its decoupling properties, you can also achieve the same with redux-observable which uses the name to describe the exact same pattern, but with RxJS. If you're already familiar with Rx, you'll feel right at home.epic
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
Some redux-saga useful resources
2017 advises
- Don't overuse Redux-saga just for the sake of using it. Testable API calls only are not worth it.
- Don't remove thunks from your project for most simple cases.
- Don't hesitate to dispatch thunks in if it makes sense.
yield put(someActionThunk)
If you are frightened of using Redux-saga (or Redux-observable) but just need the decoupling pattern, check redux-dispatch-subscribe: it permits to listen to dispatches and trigger new dispatches in listener.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});