const notificationHide = createLogic({
// the action type that will trigger this logic
type: 'NOTIFICATION_DISPLAY',
// your business logic can be applied in several
// execution hooks: validate, transform, process
// We are defining our code in the process hook below
// so it runs after the action hit reducers, hide 5s later
process({ getState, action }, dispatch) {
setTimeout(() => {
dispatch({ type: 'NOTIFICATION_CLEAR' });
}, 5000);
}
});
//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {
//If your action doesn't have any timeout attribute, fallback to the default handler
if(!action.timeout) {
return next (action)
}
const defaultTimeoutDuration = 1000;
const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;
//timeout here is called based on the duration defined in the action.
setTimeout(() => {
next (action)
}, timeoutDuration)
}
function* toastSaga() {
// Some config constants
const MaxToasts = 3;
const ToastDisplayTime = 4000;
// Local generator state: you can put this state in Redux store
// if it's really important to you, in my case it's not really
let pendingToasts = []; // A queue of toasts waiting to be displayed
let activeToasts = []; // Toasts currently displayed
// Trigger the display of a toast for 4 seconds
function* displayToast(toast) {
if ( activeToasts.length >= MaxToasts ) {
throw new Error("can't display more than " + MaxToasts + " at the same time");
}
activeToasts = [...activeToasts,toast]; // Add to active toasts
yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
yield call(delay,ToastDisplayTime); // Wait 4 seconds
yield put(events.toastHidden(toast)); // Hide the toast
activeToasts = _.without(activeToasts,toast); // Remove from active toasts
}
// Everytime we receive a toast display request, we put that request in the queue
function* toastRequestsWatcher() {
while ( true ) {
// Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
const newToast = event.data.toastData;
pendingToasts = [...pendingToasts,newToast];
}
}
// We try to read the queued toasts periodically and display a toast if it's a good time to do so...
function* toastScheduler() {
while ( true ) {
const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
if ( canDisplayToast ) {
// We display the first pending toast of the queue
const [firstToast,...remainingToasts] = pendingToasts;
pendingToasts = remainingToasts;
// Fork means we are creating a subprocess that will handle the display of a single toast
yield fork(displayToast,firstToast);
// Add little delay so that 2 concurrent toast requests aren't display at the same time
yield call(delay,300);
}
else {
yield call(delay,50);
}
}
}
// This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
yield [
call(toastRequestsWatcher),
call(toastScheduler)
]
}
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
// Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
// for the notification that is not currently visible.
// Alternatively, we could store the timeout ID and call
// clearTimeout(), but we’d still want to do it in a single place.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
// store.js
export default createStore(reducer)
// actions.js
import store from './store'
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
const id = nextNotificationId++
store.dispatch(showNotification(id, text))
setTimeout(() => {
store.dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout('You just logged in.')
// otherComponent.js
showNotificationWithTimeout('You just logged out.')
这看起来更简单但是 we don’t recommend this approach . 我们不喜欢它的主要原因是因为 it forces store to be a singleton . 这使得实现server rendering非常困难 . 在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据 .
这是 finding a way to “legitimize” this pattern of providing dispatch to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creators 的动机,而不是完全不同的功能 .
因为我之前告诉你的 . If Redux Thunk middleware is enabled, any time you attempt to dispatch a function instead of an action object, the middleware will call that function with dispatch method itself as the first argument .
所以我们可以这样做:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Unlike in a regular action creator, we can exit early in a thunk
// Redux doesn’t care about its return value (or lack of it)
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
const rootEpic = (action$) => {
// sets the incoming constant as a stream
// of actions with type NEW_NOTIFICATION
const incoming = action$.ofType(NEW_NOTIFICATION)
// Merges the "incoming" stream with the stream resulting for each call
// This functionality is similar to flatMap (or Promise.all in some way)
// It creates a new stream with the values of incoming and
// the resulting values of the stream generated by the function passed
// but it stops the merge when incoming gets a new value SO!,
// in result: no quitNotification action is set in the resulting stream
// in case there is a new alert
const outgoing = incoming.switchMap((action) => {
// creates of observable with the value passed
// (a stream with only one node)
return Observable.of(quitNotification())
// it waits before sending the nodes
// from the Observable.of(...) statement
.delay(NOTIFICATION_TIMEOUT)
});
// we return the resulting stream
return outgoing;
}
13 回答
为什么要这么难?这只是UI逻辑 . 使用专门的操作来设置通知数据:
以及显示它的专用组件:
在这种情况下,问题应该是“你如何清理旧状态?”,“如何通知组件时间已经改变”
您可以实现一些TIMEOUT操作,该操作在组件的setTimeout上调度 .
也许只要显示新通知就可以清理它 .
无论如何,某处应该有一些
setTimeout
,对吗?为什么不在组件中执行此操作动机是“通知淡出”功能实际上是UI关注点 . 因此,它简化了对业务逻辑的测试 .
测试它是如何实现似乎没有意义 . 只有在通知超时时才有意义 . 因此,存根,更快的测试,更清晰的代码的代码更少 .
根据Redux Thunk文档,使用 Redux Thunk (适用于Redux的流行中间件)的正确方法是:
所以基本上它返回一个函数,你可以延迟你的调度或将它置于一个条件状态 .
所以这样的事情会为你做的工作:
这很简单 . 使用trim-redux包并在componentDidMout或其他地方写这样并在componentWillUnmount中将其删除 .
在尝试各种流行的方法(动作创作者,thunk,传奇,史诗,效果,自定义中间件)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,Where do I put my business logic in a React/Redux application?
与此处的讨论非常相似,我试图对比并比较各种方法 . 最终,它让我引入了一个新的图书馆redux-logic,它从史诗,传奇,自定义中间件中获取灵感 .
它允许您拦截操作以验证,验证,授权,以及提供执行异步IO的方法 .
一些常见功能可以简单地声明为去抖动,限制,取消,并且仅使用来自最新请求的响应(takeLatest) . redux-logic包装您的代码,为您提供此功能 .
这使您可以随心所欲地实现核心业务逻辑 . 除非您愿意,否则不必使用可观察量或生成器 . 使用函数和回调,promises,异步函数(async / await)等 .
做一个简单的5s通知的代码是这样的:
我在我的仓库中有一个更高级的通知示例,其工作方式类似于Sebastian Lorber所描述的,您可以将显示限制为N个项目并旋转排队的任何项目 . redux-logic notification example
我有各种各样的redux-logic jsfiddle live examples as well as full examples . 我将继续研究文档和示例 .
我很乐意听取您的反馈意见 .
如果要对选择性操作进行超时处理,可以尝试middleware方法 . 我有选择地处理基于承诺的行为遇到了类似的问题,这个解决方案更加灵活 .
让我们说你的动作创建者看起来像这样:
timeout可以在上面的操作中保存多个值
以毫秒为单位的数字 - 表示特定的超时持续时间
true - 持续超时持续时间 . (在中间件中处理)
undefined - 立即发送
您的中间件实现如下所示:
您现在可以使用redux通过此中间件层路由所有操作 .
你可以找到一些类似的例子here
使用Redux-saga
正如Dan Abramov所说,如果您想要对异步代码进行更高级的控制,可以查看redux-saga .
这个答案是一个简单的例子,如果你想更好地解释为什么redux-saga对你的应用程序有用,请查看 this other answer.
一般的想法是Redux-saga提供了一个ES6生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么你经常在Redux-saga中找到无限的while循环) . 不知何故,Redux-saga正在Javascript中直接构建自己的语言 . Redux-saga起初可能感觉有点难学,因为你需要基本的理解生成器,但也了解Redux-saga提供的语言 .
我将在这里尝试描述我在redux-saga之上构建的通知系统 . 此示例目前在 生产环境 中运行 .
高级通知系统规范
您可以请求显示通知
您可以请求隐藏通知
通知不应超过4秒
可以同时显示多个通知
可以同时显示不超过3个通知
如果在已显示3个通知的情况下请求通知,则排队/推迟通知 .
结果
我的 生产环境 应用程序的屏幕截图Stample.co
代码
在这里,我将通知命名为
toast
,但这是一个命名细节 .还原剂:
用法
您只需发送
TOAST_DISPLAY_REQUESTED
事件即可 . 如果您发送4个请求,则只会显示3个通知,第1个通知消失后,第4个通知会稍后显示 .请注意,我不特别建议从JSX调度
TOAST_DISPLAY_REQUESTED
. 您宁愿添加另一个可以侦听已存在的应用事件的传奇,然后调度TOAST_DISPLAY_REQUESTED
:触发通知的组件,不必与通知系统紧密耦合 .结论
我的代码并不完美,但在 生产环境 中运行了0个bug . Redux-saga和生成器最初有点难,但是一旦你理解它们,这种系统很容易构建 .
实现更复杂的规则甚至很容易,例如:
当通知太多"queued"时,为每个通知提供更少的显示时间,以便更快地减少队列大小 .
检测窗口大小更改,并相应地更改显示的通知的最大数量(例如,桌面= 3,手机肖像= 2,手机格局= 1)
恭顺,祝你好运用thunks正确实现这类东西 .
注意你可以用redux-observable做同样的事情,这与redux-saga非常相似 . 它几乎是相同的,是发电机和RxJS之间的品味问题 .
包含示例项目的存储库
目前有四个示例项目:
Writing Async Code Inline
Extracting Async Action Creator
Use Redux Thunk
Use Redux Saga
接受的答案很棒 .
但是缺少一些东西:
没有可运行的示例项目,只是一些代码片段 .
没有其他替代方案的示例代码,例如:
Redux Saga
所以我创建了Hello Async存储库来添加缺少的东西:
可运行的项目 . 您无需修改即可下载并运行它们 .
提供更多替代品的示例代码:
Redux Saga
Redux Loop
......
Redux Saga
已接受的答案已经为Async Code Inline,Async Action Generator和Redux Thunk提供了示例代码片段 . 为了完整起见,我提供了Redux Saga的代码片段:
行动简单而纯粹 .
组件没什么特别之处 .
Sagas基于ES6 Generators
与Redux Thunk相比
优点
你最终没有回调地狱 .
您可以轻松地测试异步流程 .
你的行为保持纯洁 .
缺点
如果上面的代码片段没有回答您的所有问题,请参阅runnable project .
我建议也看一下SAM pattern .
SAM模式主张包括“下一个动作谓词”,其中一旦模型更新(SAM模型〜减速器状态存储),其中(自动)动作(例如“通知在5秒后自动消失”)被触发 .
该模式主张一次一个地对动作和模型突变进行排序,因为模型的“控制状态”“控制”由下一个动作谓词启用和/或自动执行哪些动作 . 您根本无法预测(通常)系统在处理操作之前的状态,因此您的下一个预期操作是否允许/可能 .
所以例如代码,
不允许使用SAM,因为可以调度hideNotification操作的事实取决于成功接受值“showNotication:true”的模型 . 模型的其他部分可能会阻止它接受它,因此,没有理由触发hideNotification操作 .
我强烈建议在存储更新和模型的新控制状态之后实现适当的下一个动作谓词 . 这是实现您正在寻找的行为最安全的方法 .
如果您愿意,可以加入我们的Gitter . 还有SAM getting started guide available here .
Redux本身是一个非常冗长的库,对于这样的东西,你必须使用类似Redux-thunk的东西,这将给出一个
dispatch
功能,因此您可以在几秒钟后发送通知的结束 .I have created a library解决详细程度和可组合性等问题,您的示例将如下所示:
因此,我们编写同步操作以在异步操作中显示通知,该操作可以向后台请求某些信息,或稍后检查通知是否已手动关闭 .
不要陷入trap of thinking a library should prescribe how to do everything . 如果您想在JavaScript中执行超时操作,则需要使用
setTimeout
. Redux的行动没有任何理由应该有所不同 .Redux确实提供了一些处理异步内容的替代方法,但是只有在意识到重复代码太多时才应该使用它们 . 除非您遇到此问题,否则请使用该语言提供的内容并选择最简单的解决方案 .
编写异步代码内联
这是迄今为止最简单的方法 . 这里没有Redux特有的东西 .
同样,从连接组件内部:
唯一的区别是,在连接组件中,您通常无法访问商店本身,但可以将
dispatch()
或特定的动作创建者注入为道具 . 然而,这对我们没有任何影响 .如果您不喜欢在从不同组件分派相同操作时进行拼写错误,则可能需要提取操作创建者,而不是内联调度操作对象:
或者,如果您之前使用
connect()
绑定它们:到目前为止,我们还没有使用任何中间件或其他高级概念 .
提取异步动作创建器
上面的方法在简单的情况下工作正常,但您可能会发现它有一些问题:
它强制您在要显示通知的任何位置复制此逻辑 .
通知没有ID,因此如果您足够快地显示两个通知,则会出现竞争条件 . 当第一个超时完成时,它将调度
HIDE_NOTIFICATION
,错误地比超时后错误地隐藏第二个通知 .要解决这些问题,您需要提取一个集中超时逻辑并调度这两个操作的函数 . 它可能看起来像这样:
现在,组件可以使用
showNotificationWithTimeout
而无需复制此逻辑或具有不同通知的竞争条件:为什么
showNotificationWithTimeout()
接受dispatch
作为第一个参数?因为它需要将操作分派给商店 . 通常,组件可以访问dispatch
,但由于我们希望外部函数控制调度,因此我们需要控制调度 .如果您从某个模块导出单件商店,则可以直接导入它并直接在其上导入
dispatch
:这看起来更简单但是 we don’t recommend this approach . 我们不喜欢它的主要原因是因为 it forces store to be a singleton . 这使得实现server rendering非常困难 . 在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据 .
单身商店也使测试更加困难 . 在测试动作创建者时,您不能再模拟商店,因为它们引用从特定模块导出的特定实体商店 . 你甚至无法从外面重置它的状态 .
因此,虽然您在技术上可以从模块中导出单件商店,但我们不鼓励它 . 除非您确定您的应用永远不会添加服务器渲染,否则请不要这样做 .
回到以前的版本:
这解决了重复逻辑的问题,并使我们免于竞争条件 .
Thunk中间件
对于简单的应用程序,该方法应该足够了 . 如果您对它感到满意,请不要担心中间件 .
但是,在较大的应用中,您可能会发现一些不便之处 .
例如,我们不得不绕过
dispatch
似乎很不幸 . 这使得separate container and presentational components变得更加棘手,因为以上述方式异步调度Redux动作的任何组件必须接受dispatch
作为prop,以便它可以进一步传递它 . 您不能再将动作创建者与connect()
绑定,因为showNotificationWithTimeout()
实际上不是动作创建者 . 它不会返回Redux操作 .此外,记住哪些函数是
showNotification()
等同步动作创建者以及showNotificationWithTimeout()
之类的异步助手可能很难 . 你必须以不同的方式使用它们,并注意不要互相误解 .这是 finding a way to “legitimize” this pattern of providing dispatch to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creators 的动机,而不是完全不同的功能 .
如果您仍然和我们在一起,并且您也认为您的应用程序存在问题,欢迎您使用Redux Thunk中间件 .
在一个要点中,Redux Thunk教Redux识别实际上具有功能的特殊动作:
启用此中间件时, if you dispatch a function ,Redux Thunk中间件会将
dispatch
作为参数 . 它也会“吞下”这样的动作,所以不要担心你的减速器接收到奇怪的函数参数 . 你的减速器只接收普通物体动作 - 直接发射,或者由我们刚刚描述的功能发出 .这看起来不是很有用,是吗?不是在这种特殊情况下 . 但是,它允许我们将
showNotificationWithTimeout()
声明为常规Redux操作创建者:请注意该函数与我们在上一节中编写的函数几乎完全相同 . 但是它不接受
dispatch
作为第一个参数 . 相反,它返回一个接受dispatch
作为第一个参数的函数 .我们如何在我们的组件中使用它?当然,我们可以这样写:
我们调用异步操作创建器来获取只需要
dispatch
的内部函数,然后我们传递dispatch
.然而,这比原始版本更加尴尬!为什么我们甚至走那条路?
因为我之前告诉你的 . If Redux Thunk middleware is enabled, any time you attempt to dispatch a function instead of an action object, the middleware will call that function with dispatch method itself as the first argument .
所以我们可以这样做:
最后,调度异步操作(实际上是一系列操作)与将同一个操作同步分派给组件没有什么不同 . 这是好事,因为组件不应该关心某些事情是同步发生还是异步发生 . 我们只是把它抽象出去了 .
请注意,由于我们“教”Redux识别这样的“特殊”动作创建者(我们称之为thunk动作创建者),我们现在可以在任何我们使用常规动作创建者的地方使用它们 . 例如,我们可以将它们与
connect()
一起使用:在Thunk中读取状态
通常,您的reducer包含用于确定下一个状态的业务逻辑 . 但是,只有在调度动作后才会启动减速器 . 如果您在thunk动作创建者中有副作用(例如调用API),并且您想在某些条件下阻止它,该怎么办?
不使用thunk中间件,你只需在组件内部进行检查:
但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件上 . 幸运的是,Redux Thunk为您提供了一种读取Redux商店当前状态的方法 . 除了
dispatch
之外,它还将getState
作为您从thunk动作创建者返回的函数的第二个参数传递 . 这让thunk读取商店的当前状态 .不要滥用这种模式 . 当存在可用的缓存数据时,最好避免API调用,但它不是构建业务逻辑的良好基础 . 如果仅使用
getState()
有条件地分派不同的操作,请考虑将业务逻辑放入reducers中 .后续步骤
既然您对thunks的工作方式有了基本的直觉,请查看使用它们的Redux async example .
你可能会发现许多thunk返回Promises的例子 . 这不是必需的,但可以非常方便 . Redux并不关心你从thunk返回什么,但它会从
dispatch()
给你它的返回值 . 这就是为什么你可以从thunk返回一个Promise并等待它通过调用dispatch(someThunkReturningPromise()).then(...)
来完成 .您也可以将复杂的thunk动作创建者分成几个较小的thunk动作创建者 . thunks提供的
dispatch
方法本身可以接受thunk,因此可以递归地应用该模式 . 同样,这最适合Promises,因为您可以在其上实现异步控制流 .对于某些应用程序,您可能会发现自己的异步控制流要求太复杂而无法用thunk表示 . 例如,重试失败的请求,带令牌的重新授权流程或逐步入门可能过于冗长且以这种方式编写时容易出错 . 在这种情况下,您可能希望查看更高级的异步控制流解决方案,例如Redux Saga或Redux Loop . 评估它们,比较与您的需求相关的示例,并选择您最喜欢的那个 .
最后,如果你没有真正的需要,不要使用任何东西(包括thunk) . 请记住,根据要求,您的解决方案可能看起来很简单
除非你知道为什么要这样做,否则不要流汗 .
我知道这个问题有点旧,但我将使用 redux-observable aka引入另一个解决方案 . 史诗 .
引用官方文档:
什么是redux-observable?
Epic是redux-observable的核心原语 .
或多或少,您可以创建一个通过Stream接收操作的函数,然后返回一个新的操作流(使用常见的副作用,如超时,延迟,间隔和请求) .
让我发布代码,然后再解释一下它
store.js
index.js
App.js
解决这个问题的关键代码就像你看到的那样简单,唯一与其他答案不同的是函数rootEpic .
要点1.与传奇一样,你必须结合史诗为了获得接收操作流并返回操作流的顶级函数,您可以将其与中间件工厂createEpicMiddleware一起使用 . 在我们的例子中,我们只需要一个,所以我们只有rootEpic,所以我们不知道这个事实 .
要点2.我们关注副作用逻辑的rootEpic只需要大约5行代码就可以了!包括几乎是声明的事实!
点3.逐行rootEpic解释(在评论中)
我希望它有所帮助!
您可以使用redux-thunk执行此操作 . 像setTimeout这样的异步操作有guide in redux document .
每当你执行setTimeout时,请确保在componentWillUnMount生命周期方法中卸载组件时使用clearTimeout清除超时