根据文件,"Without middleware, Redux store only supports synchronous data flow" . 我不是't understand why this is the case. Why can' t容器组件调用异步API,然后 dispatch
动作?
例如,想象一个简单的UI:字段和按钮 . 当用户按下按钮时,该字段将填充来自远程服务器的数据 .
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
渲染导出的组件时,我可以单击该按钮并正确更新输入 .
注意 connect
调用中的 update
函数 . 它调度一个动作,告诉应用程序它正在更新,然后执行异步调用 . 调用完成后,将提供的值作为另一个操作的有效负载进行调度 .
这种方法有什么问题?我想为什么要使用Redux Thunk或Redux Promise,正如文档所示?
EDIT: 我搜索了Redux仓库以获取线索,并发现Action Creators过去必须是纯函数 . 例如,here's a user trying to provide a better explanation for async data flow:
动作创建者本身仍然是一个纯函数,但它返回的thunk函数不需要,它可以执行我们的异步调用
Action creators are no longer required to be pure.所以,thunk / promise中间件在过去肯定是必需的,但似乎不再是这种情况了?
7 回答
你没有 .
但是......你应该使用redux-saga :)
Dan Abramov的回答是对的
redux-thunk
但我会更多地谈论redux-saga这是非常相似但更强大的 .势在必行VS声明
DOM :jQuery是必需的/ React是声明性的
Monads :IO必不可少/免费是声明性的
Redux effects :
redux-thunk
势在必行/redux-saga
是声明性的如果你手上有一个thunk,比如IO monad或promise,你就不能轻易知道一旦你执行它会做什么 . 测试thunk的唯一方法是执行它,并模拟调度程序(如果它与更多东西交互,则模拟整个外部世界......) .
如果您使用的是模拟,那么您就不会进行函数式编程 .
Source
sagas(因为它们在
redux-saga
中实现)是声明性的,就像Free monad或React组件一样,它们更容易测试而不需要任何模拟 .另见article:
(实际上,Redux-saga就像一个混合体:流程势在必行,但效果是声明性的)
混淆:动作/事件/命令......
关于CQRS / EventSourcing和Flux / Redux等后端概念如何相关,前端世界存在很多混淆,主要是因为在Flux中我们使用术语"action",它有时代表命令式代码(
LOAD_USER
)和事件(USER_LOADED
) ) . 我相信像事件采购一样,你应该只发送事件 .在实践中使用传奇
想象一个带有用户 Profiles 链接的应用 . 使用这两个中间件处理此问题的惯用方法是:
redux-thunk
redux-saga
这个传奇转换为:
如您所见,
redux-saga
有一些优点 .使用
takeLatest
允许表示您只对获取上次用户名的数据感兴趣(如果用户在很多用户名上非常快速地点击,则处理并发问题) . 这种东西很难用thunks . 如果您不想要这种行为,可以使用takeEvery
.你让动作创作者保持纯洁 . 请注意,保留actionCreators(在sagas
put
和组件dispatch
中)仍然很有用,因为它可能有助于您将来添加操作验证(assertions / flow / typescript) .由于效果是声明性的,因此您的代码变得更加可测试
您不再需要触发像
actions.loadUser()
这样的类似rpc的调用 . 您的UI只需要发送已经发生的事情 . 我们只发射 events (总是以过去时态!)而不再是行动了 . 这意味着您可以创建解耦的"ducks"或Bounded Contexts,并且saga可以充当这些模块化组件之间的耦合点 .这意味着您的视图更易于管理,因为他们不再需要在已发生的事件和作为效果的事件之间包含该转换层
例如,想象一个无限滚动视图 .
CONTAINER_SCROLLED
可以导致NEXT_PAGE_LOADED
,但是可滚动容器的责任是决定我们是否应该加载另一个页面?然后他必须意识到更多复杂的事情,比如最后一页是否成功加载,或者是否已经有一个试图加载的页面,或者是否还有剩余的项目要加载?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已滚动 . 页面的加载是该卷轴的"business effect"有些人可能会争辩说,生成器可以通过局部变量固有地隐藏在redux存储区之外的状态,但是如果你开始通过启动计时器等在thunk内部编排复杂的事情,那么无论如何都会遇到同样的问题 . 并且有一个
select
效果,现在允许从你的Redux商店获得一些状态 .Sagas可以进行时间旅行,并且还可以实现当前正在进行的复杂流量记录和开发工具 . 以下是一些已经实现的简单异步流日志:
解耦
Sagas不仅取代了redux thunk . 它们来自后端/分布式系统/事件源 .
这是一个非常普遍的误解,传奇只是在这里以更好的可测试性取代你的redux thunk . 实际上这只是redux-saga的一个实现细节 . 使用声明效果优于thunks的可测试性,但是saga模式可以在命令式或声明性代码之上实现 .
首先,saga是一个允许协调长期运行事务(最终一致性)和跨不同有界上下文(域驱动设计术语)的事务的软件 .
为了简化前端世界,假设有widget1和widget2 . 当单击widget1上的某个按钮时,它应该对widget2有影响 . 而不是将2个小部件耦合在一起(即widget1调度以widget2为目标的动作),widget1仅调度其按钮被单击 . 然后saga监听此按钮单击,然后通过分析widget2知道的新事件来更新widget2 .
这增加了简单应用程序不必要的间接级别,但可以更轻松地扩展复杂的应用程序 . 您现在可以将widget1和widget2发布到不同的npm存储库,这样他们就不必了解彼此,也无需共享全局操作注册表 . 这两个小部件现在是有限的上下文,可以单独生活 . 他们不需要彼此保持一致,也可以在其他应用程序中重复使用 . 传奇是两个小部件之间的耦合点,它们以有意义的方式为您的业务协调它们 .
关于如何构建Redux应用程序的一些很好的文章,您可以使用Redux-saga来解耦:
http://jaysoo.ca/2016/02/28/organizing-redux-application/
http://marmelab.com/blog/2015/12/17/react-directory-structure.html
https://github.com/slorber/scalable-frontend-with-elm-or-redux
具体用例:通知系统
我希望我的组件能够触发应用内通知的显示 . 但我不希望我的组件高度耦合到具有自己的业务规则的通知系统(同时显示最多3个通知,通知排队,4秒显示时间等...) .
我不希望我的JSX组件决定何时显示/隐藏通知 . 我只是让它能够请求通知,并将复杂的规则留在传奇中 . 这种东西很难用thunks或promises来实现 .
我已经描述了here如何用saga做到这一点
为什么称它为传奇?
传奇这个词来自后端世界 . 我最初在long discussion中将Yassine(Redux-saga的作者)引入该术语 .
最初,该术语是用paper引入的,saga模式应该用于处理分布式事务中的最终一致性,但它的用法已经扩展到后端开发人员更广泛的定义,因此它现在也涵盖了"process manager"模式(不知何故原始的传奇模式是一种特殊形式的流程管理器) .
今天,术语"saga"令人困惑,因为它可以描述两种不同的东西 . 正如它在redux-saga中使用的那样,它没有描述处理分布式事务的方法,而是一种协调应用程序中的操作的方法 .
redux-saga
也可以被称为redux-process-manager
.也可以看看:
Interview of Yassine about Redux-saga history
Kella Byte: Claryfing the Saga pattern
Microsoft CQRS Journey: A Saga on Sagas
Medium response of Yassine
替代品
如果您不喜欢使用生成器的想法但是您对saga模式及其解耦属性感兴趣,那么您也可以使用redux-observable来实现相同的功能,它使用名称
epic
来描述完全相同的模式,但使用RxJS . 如果你感到宾至如归 .一些redux-saga有用的资源
Redux-saga vs Redux-thunk with async/await
Managing processes in Redux Saga
From actionsCreators to Sagas
Snake game implemented with Redux-saga
2017建议
不要仅仅为了使用它而过度使用Redux-saga . 可测试的API调用仅值得 .
对于大多数简单情况,请勿从项目中删除thunk .
如果有意义,请毫不犹豫地在
yield put(someActionThunk)
发送雷鸣声 .如果您害怕使用Redux-saga(或Redux-observable),但只需要解耦模式,检查redux-dispatch-subscribe:它允许侦听调度并在侦听器中触发新的调度 .
要回答开头提出的问题:
请记住,这些文档适用于Redux,而不是Redux和React . 连接到React组件的Redux存储可以完全按照您的说法执行,但是没有中间件的Plain Jane Redux存储不接受
dispatch
的参数,除了普通的OL'对象 .没有中间件,你当然可以做到
但这是一个类似的情况,其中异步是围绕Redux而不是由Redux处理 . 因此,中间件通过修改可以直接传递给
dispatch
的内容来允许异步 .也就是说,我认为你的建议的精神是有效的 . 在Redux React应用程序中,您肯定可以使用其他方法来处理异步 .
使用中间件的一个好处是,您可以继续正常使用动作创建器,而不必担心它们是如何连接起来的 . 例如,使用
redux-thunk
,您编写的代码看起来很像它没有改变 - 并且
connect
不知道updateThing
是(或需要)异步的 .如果您还想支持promises,observables,sagas或crazy custom和highly declarative动作创建者,那么Redux只需将您传递的内容更改为
dispatch
(也就是您从动作创建者返回的内容)即可 . 不需要使用React组件(或connect
调用) .理想情况下,Abramov 's goal - and everyone'只是将复杂性(和异步调用)封装在最合适的地方 .
在标准的Redux数据流中,哪里是最佳选择?怎么样:
Reducers ?没门 . 它们应该是纯粹的功能,没有副作用 . 更新商店是严肃,复杂的业务 . 不要污染它 .
Dumb View Components? 绝对不会 . 他们有一个问题:演示和用户互动,应该尽可能简单 .
Container Components? 可能,但次优 . 有意义的是,容器是我们封装一些与视图相关的复杂性并与商店交互的地方,但是:
容器确实需要比哑组件更复杂,但它仍然是一个单一的责任:提供视图和状态/存储之间的绑定 . 你的异步逻辑是一个完全独立的问题 .
通过将它放在一个容器中,你可以重复使用,并完全解耦 .
S ome other Service Module? 糟糕的主意:你需要注入商店的访问权限,这是一个可维护性/可测试性的噩梦 . 最好使用Redux并使用提供的API /模型访问商店 .
Actions and the Middlewares that interpret them? 为什么不呢?!对于初学者来说,'s the only major option we have left. :-) More logically, the action system is decoupled execution logic that you can use from anywhere. It'可以访问商店并可以发送更多动作 . 它有一个单一的责任,即组织应用程序周围的控制和数据流,大多数异步适合于此 .
动作创作者怎么样?为什么不在那里做异步,而不是在动作本身和中间件?
首先也是最重要的,创作者不会发送新的或有行动,无法从商店读取构成您的异步等 .
所以,要把复杂性放在一个复杂的必需品的地方,并保持其他一切都很简单 . 然后,创建者可以是简单,相对纯粹的功能,易于测试 .
这种方法没有错 . 这在大型应用程序中只是不方便,因为您将有不同的组件执行相同的操作,您可能想要去除某些操作,或保持一些本地状态,如自动递增ID靠近动作创建者等 . 所以它更容易从维护的角度来将动作创建者提取到单独的函数中 .
You can read my answer to “How to dispatch a Redux action with a timeout” for a more detailed walkthrough.
像Redux Thunk或Redux Promise这样的中间件只为你提供“语法糖”来发送thunk或promises,但是你不必使用它 .
因此,没有任何中间件,您的动作创建者可能看起来像
但是使用Thunk Middleware你可以像这样写:
所以没有太大的区别 . 我喜欢后一种方法的一件事是组件并不关心动作创建者是异步的 . 它通常只调用
dispatch
,它也可以使用mapDispatchToProps
用短语法绑定这样的动作创建器等 . 组件不知道如何实现动作创建器,你可以在不同的异步方法之间切换(Redux Thunk,Redux Promise, Redux Saga)无需更换组件 . 另一方面,使用前一种显式方法,您的组件确切地知道特定调用是异步的,并且需要通过某种约定传递dispatch
(例如,作为同步参数) .还要考虑一下这段代码的用法更改 . 假设我们想要第二个数据加载功能,并将它们组合在一个动作创建器中 .
使用第一种方法,我们需要注意我们正在调用什么样的动作创建者:
使用Redux Thunk动作创建者可以
dispatch
其他动作创建者的结果,甚至不认为这些是同步还是异步:使用这种方法,如果您以后希望您的动作创建者查看当前的Redux状态,您可以使用传递给thunk的第二个
getState
参数,而无需修改调用代码:如果您需要将其更改为同步,您也可以在不更改任何调用代码的情况下执行此操作:
因此,使用Redux Thunk或Redux Promise等中间件的好处是,组件不了解动作创建者的实现方式,以及他们是否关心Redux状态,是同步还是异步,以及他们是否称其他动作创建者 . 缺点是一点间接,但我们相信它在实际应用中是值得的 .
最后,Redux Thunk和朋友只是Redux应用程序中异步请求的一种可能方法 . 另一个有趣的方法是Redux Saga,它允许您定义长时间运行的守护进程(“sagas”),它们在进行操作时执行操作,并在输出操作之前转换或执行请求 . 这将逻辑从动作创作者转变为传奇 . 您可能想要查看它,然后选择最适合您的选项 .
这是不正确的 . 文档说这个,但文档错了 .
行动创造者从未被要求成为纯粹的职能 .
我们修改了文档来反映这一点 .
使用Redux-saga是React-redux实现中最好的中间件 .
例如:store.js
然后是saga.js
然后是action.js
然后reducer.js
然后是main.js
试试这个......工作正常
The short answer :对我来说,这似乎是一种完全合理的异步问题 . 有几个警告 .
在我刚开始工作的新项目上,我有一个非常相似的思路 . 我是香草Redux优雅系统的忠实粉丝,用于更新商店和重新渲染组件,使其远离React组件树的内核 . 对我来说,加入优雅的
dispatch
机制以处理异步似乎很奇怪 .我最终采用了一种非常类似的方法来处理我在项目中所拥有的内容,我从项目中考虑了这一点,我们称之为react-redux-controller .
由于以下几个原因,我最终没有采用上面的确切方法:
你编写它的方式,那些调度函数不会认为这会将这些UI组件与调度逻辑不必要地耦合在一起 . 更有问题的是,调度功能没有明显的方法来访问异步延续中的更新状态 .
调度函数可以通过词法范围访问
dispatch
. 一旦connect
语句失控,这就限制了重构的选项 - 而且只有一个update
方法看起来非常笨重 . 因此,如果将它们分解为单独的模块,您需要一些系统来让您编写这些调度程序功能 .总而言之,您必须安装一些系统以允许
dispatch
并将商店注入您的调度功能以及事件的参数 . 我知道这种依赖注入的三种合理方法:redux-thunk通过将它们传递到你的thunks(通过圆顶定义使它们完全不是thunks),以功能性的方式做到这一点 . 我没有使用其他
dispatch
中间件方法,但我认为它们基本相同 .react-redux-controller使用协程执行此操作 . 作为奖励,它还允许您访问"selectors",这些函数可能是作为
connect
的第一个参数传递的,而不是直接使用原始的规范化存储 .您也可以通过各种可能的机制将它们注入
this
上下文,从而实现面向对象的方式 .Update
在我看来,这个难题的一部分是react-redux的限制 . connect的第一个参数获取状态快照,但不是调度 . 第二个参数得到调度而不是状态 . 两个参数都没有得到关闭当前状态的thunk,因为能够在继续/回调时看到更新状态 .
OK, 让我们开始看看中间件如何工作,这完全回答了这个问题,这是Redux中的源代码 pplyMiddleWare 函数:
看看这部分,看看我们的 dispatch 如何成为 function .
好的,这就像 Redux-thunk 一样Redux最常用的中间件介绍自己:
所以如你所见,它将返回一个函数而不是一个动作,这意味着你可以随时随地调用它,因为它是一个函数......
那真是太糟糕了?这就是它在维基百科中的介绍:
所以看看概念是多么容易,以及它如何帮助您管理异步操作......
没有它你就可以生活,但在编程中记住,总有更好,更整洁,更恰当的方法来做事......