如何调度具有超时的Redux操作?

我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后,我需要在5秒后调度另一个操作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。

我没有运气使用setTimeout并返回另一个操作,也找不到这是如何在线完成的。所以欢迎任何建议。

501181 次浏览

您可以使用redux-thunk做到这一点。有一个redux文档中的指南用于异步操作,例如setTimeout。

不要陷入认为图书馆应该规定如何做每件事的陷阱。如果你想在JavaScript中使用超时做某事,你需要使用setTimeout。Redux操作没有理由有任何不同。

Redux确实提供了一些处理异步内容的替代方法,但是你应该只在意识到你重复了太多代码时使用这些方法。除非你有这个问题,否则使用该语言提供的并选择最简单的解决方案。

内联编写异步代码

这是目前为止最简单的方法。这里没有Redux的具体内容。

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })setTimeout(() => {store.dispatch({ type: 'HIDE_NOTIFICATION' })}, 5000)

类似地,从连接组件内部:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })setTimeout(() => {this.props.dispatch({ type: 'HIDE_NOTIFICATION' })}, 5000)

唯一的区别是,在连接的组件中,您通常无法访问商店本身,而是将dispatch()或特定操作创建者作为道具注入。然而,这对我们没有任何区别。

如果您不喜欢在从不同组件调度相同的操作时拼写错误,您可能希望提取操作创建者而不是内联调度操作对象:

// actions.jsexport function showNotification(text) {return { type: 'SHOW_NOTIFICATION', text }}export function hideNotification() {return { type: 'HIDE_NOTIFICATION' }}
// component.jsimport { showNotification, hideNotification } from '../actions'
this.props.dispatch(showNotification('You just logged in.'))setTimeout(() => {this.props.dispatch(hideNotification())}, 5000)

或者,如果您之前将它们与connect()绑定:

this.props.showNotification('You just logged in.')setTimeout(() => {this.props.hideNotification()}, 5000)

到目前为止,我们还没有使用任何中间件或其他高级概念。

提取异步动作创建器

上述方法在简单情况下运行良好,但您可能会发现它存在一些问题:

  • 它强制您在任何想要显示通知的地方复制此逻辑。
  • 通知没有ID,所以如果你足够快地显示两个通知,你将有一个竞争条件。当第一个超时完成时,它将调度HIDE_NOTIFICATION,错误地隐藏第二个通知早于超时后。

要解决这些问题,你需要提取一个集中超时逻辑并分派这两个操作的函数。它可能如下所示:

// actions.jsfunction showNotification(id, text) {return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {return { type: 'HIDE_NOTIFICATION', id }}
let nextNotificationId = 0export 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)}

现在组件可以使用showNotificationWithTimeout而无需复制此逻辑或具有不同通知的竞争条件:

// component.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

为什么showNotificationWithTimeout()接受dispatch作为第一个参数?因为它需要将操作调度到商店。通常一个组件可以访问dispatch,但由于我们希望外部函数控制调度,我们需要让它控制调度。

如果你有一个从某个模块导出的单例存储,你可以直接导入它并dispatch

// store.jsexport default createStore(reducer)
// actions.jsimport store from './store'
// ...
let nextNotificationId = 0export function showNotificationWithTimeout(text) {const id = nextNotificationId++store.dispatch(showNotification(id, text))
setTimeout(() => {store.dispatch(hideNotification(id))}, 5000)}
// component.jsshowNotificationWithTimeout('You just logged in.')
// otherComponent.jsshowNotificationWithTimeout('You just logged out.')

这看起来更简单,但我们不推荐这种方法。我们不喜欢它的主要原因是它迫使store成为单例。这使得实现服务器渲染非常困难。在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。

单例存储也使测试更加困难。在测试操作创建者时,您不能再模拟存储,因为它们引用了从特定模块导出的特定真实存储。您甚至不能从外部重置其状态。

因此,虽然您在技术上可以从模块导出单例存储,但我们不鼓励这样做。除非您确定您的应用程序永远不会添加服务器渲染,否则请不要这样做。

回到上一个版本:

// actions.js
// ...
let nextNotificationId = 0export function showNotificationWithTimeout(dispatch, text) {const id = nextNotificationId++dispatch(showNotification(id, text))
setTimeout(() => {dispatch(hideNotification(id))}, 5000)}
// component.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

这解决了逻辑重复的问题,并使我们免于竞争条件。

Thunk中间件

对于简单的应用程序,这种方法应该足够了。如果您对中间件感到满意,请不要担心它。

但是,在较大的应用程序中,您可能会发现它周围存在某些不便。

例如,我们必须传递dispatch似乎很不幸。这使得单独的容器和表示组件变得更加棘手,因为任何以上述方式异步调度Redux操作的组件都必须接受dispatch作为道具,以便它可以进一步传递它。你不能再仅仅将动作创建者与connect()绑定,因为showNotificationWithTimeout()不是真正的动作创建者。它不会返回Redux操作。

此外,要记住哪些函数是同步动作创建者(如showNotification()),哪些是异步助手(如showNotificationWithTimeout())可能会很尴尬。您必须以不同的方式使用它们,并小心不要相互混淆。

这是找到一种方法来“合法化”这种向帮助函数提供#0的模式,并帮助Redux“看到”这种异步动作创建者作为普通动作创建者的特殊情况的动机,而不是完全不同的功能。

如果您仍然与我们在一起,并且您也发现您的应用程序存在问题,欢迎您使用ReduxThunk中间件。

简而言之,Redux Thunk教Redux识别实际上是函数的特殊类型的操作:

import { createStore, applyMiddleware } from 'redux'import thunk from 'redux-thunk'
const store = createStore(reducer,applyMiddleware(thunk))
// It still recognizes plain object actionsstore.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functionsstore.dispatch(function (dispatch) {// ... which themselves may dispatch many timesdispatch({ type: 'INCREMENT' })dispatch({ type: 'INCREMENT' })dispatch({ type: 'INCREMENT' })
setTimeout(() => {// ... even asynchronously!dispatch({ type: 'DECREMENT' })}, 1000)})

当启用这个中间件时,如果你调度一个函数,Redux Thunk中间件会给它dispatch作为参数。它也会“吞下”这样的操作,所以不要担心你的简化程序会收到奇怪的函数参数。你的简化程序只会收到普通的对象操作——要么直接发出,要么像我们刚才描述的那样由函数发出。

这看起来不是很有用,是吗?在这种特殊情况下不是。然而,它让我们声明showNotificationWithTimeout()为常规的Redux操作创建者:

// actions.jsfunction showNotification(id, text) {return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {return { type: 'HIDE_NOTIFICATION', id }}
let nextNotificationId = 0export function showNotificationWithTimeout(text) {return function (dispatch) {const id = nextNotificationId++dispatch(showNotification(id, text))
setTimeout(() => {dispatch(hideNotification(id))}, 5000)}}

请注意,该函数与我们在上一节中编写的函数几乎相同。然而,它不接受dispatch作为第一个参数。相反,它是一个接受dispatch作为第一个参数的函数。

我们将如何在我们的组件中使用它?当然,我们可以这样写:

// component.jsshowNotificationWithTimeout('You just logged in.')(this.props.dispatch)

我们调用异步操作创建者来获取只需要dispatch的内部函数,然后我们传递dispatch

然而,这比原来的版本更尴尬!我们为什么要走那条路?

因为我之前告诉过你的。如果启用了Redux Thunk中间件,每当您尝试调度函数而不是操作对象时,中间件都会以#0方法本身作为第一个参数调用该函数

所以我们可以这样做:

// component.jsthis.props.dispatch(showNotificationWithTimeout('You just logged in.'))

最后,分派一个异步动作(实际上是一系列动作)看起来与同步地向组件分派一个动作没有什么不同。这很好,因为组件不应该关心某些事情是同步还是异步发生的。我们只是把它抽象出来了。

请注意,由于我们“教”Redux识别这种“特殊”动作创造者(我们称他们为Thunk动作创造者),我们现在可以在任何使用常规动作创造者的地方使用它们。例如,我们可以将它们与connect()一起使用:

// actions.js
function showNotification(id, text) {return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {return { type: 'HIDE_NOTIFICATION', id }}
let nextNotificationId = 0export function showNotificationWithTimeout(text) {return function (dispatch) {const id = nextNotificationId++dispatch(showNotification(id, text))
setTimeout(() => {dispatch(hideNotification(id))}, 5000)}}
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(mapStateToProps,{ showNotificationWithTimeout })(MyComponent)

在Thunks中读取状态

通常你的减速器包含用于确定下一个状态的业务逻辑。然而,减速器只有在调度操作后才会启动。如果你在thunk操作创建者中有一个副作用(例如调用API),并且你想在某些情况下阻止它怎么办?

如果不使用thunk中间件,您只需在组件内部执行以下检查:

// component.jsif (this.props.areNotificationsEnabled) {showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')}

然而,提取动作创建者的目的是将这种重复逻辑集中在许多组件上。幸运的是,Redux Thunk为您提供了一种方法来阅读 Redux商店的当前状态。除了dispatch之外,它还将getState作为第二个参数传递给您从thunk动作创建者返回的函数。这让thunk可以读取商店的当前状态。

let nextNotificationId = 0export 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)}}

不要滥用这种模式。当有缓存数据可用时,它很适合救助API调用,但它不是构建业务逻辑的良好基础。如果你只使用getState()来有条件地调度不同的操作,请考虑将业务逻辑放入减速器中。

后续步骤

现在您对thunks的工作原理有了基本的直觉,请查看使用它们的Redux异步示例

你可能会发现很多thunks返回Promises的例子。这不是必需的,但可以非常方便。Redux不在乎你从thunk返回什么,但它会从dispatch()给你它的返回值。这就是为什么你可以从thunk返回一个Promise,并通过调用dispatch(someThunkReturningPromise()).then(...)等待它完成。

您还可以将复杂的thunk操作创建者拆分为几个较小的thunk操作创建者。thunks提供的dispatch方法可以接受thunks本身,因此您可以递归应用该模式。同样,这最适合Promises,因为您可以在此基础上实现异步控制流。

对于某些应用程序,你可能会发现自己处于异步控制流要求过于复杂而无法用thunks表示的情况。例如,重试失败的请求、使用令牌重新授权流或逐步启动,以这种方式编写可能过于冗长且容易出错。在这种情况下,你可能希望查看更高级的异步控制流解决方案,例如Redux SagaRedux循环。评估它们,比较与你需求相关的示例,然后选择你最喜欢的一个。

最后,如果您不真正需要任何东西(包括thunks),请不要使用它们。请记住,根据需求,您的解决方案可能看起来像

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })setTimeout(() => {store.dispatch({ type: 'HIDE_NOTIFICATION' })}, 5000)

不要出汗,除非你知道你为什么要这样做。

我建议也看看SAM模式

SAM模式主张包含一个“next-action-preate”,其中(自动)操作,例如“通知在5秒后自动消失”,一旦模型更新就会触发(SAM模型~简化状态+存储)。

该模式提倡一次对一个动作和模型突变进行排序,因为模型的“控制状态”“控制”哪些动作被启用和/或由下一个动作谓词自动执行。你根本无法预测(一般来说)系统在处理一个动作之前将处于什么状态,因此你的下一个预期动作是否被允许/可能。

例如代码,

export function showNotificationWithTimeout(dispatch, text) {const id = nextNotificationId++dispatch(showNotification(id, text))
setTimeout(() => {dispatch(hideNotification(id))}, 5000)}

SAM是不允许的,因为hideNotify操作可以被分派的事实取决于模型成功接受值“Show Notication: true”。模型的其他部分可能会阻止它接受它,因此,没有理由触发hideNo的操作。

我强烈建议在商店更新之后实现一个正确的下一步操作谓词,并且可以知道模型的新控制状态。这是实现您正在寻找的行为的最安全方法。

如果你愿意,你可以在Gitter上加入我们。还有一个SAM入门指南可在此处获得

使用Redux-saga

正如Dan Abramov所说,如果你想更高级地控制你的异步代码,你可以看看redux-saga

这个答案是一个简单的例子,如果您想更好地解释为什么redux-saga对您的应用程序有用,请检查这是另一个答案

总的想法是Redux-saga提供了一个ES6生成器解释器,允许你轻松编写看起来像同步代码的异步代码(这就是为什么你经常在Redux-saga中发现无限的同时循环)。不知何故,Redux-saga正在直接在Javascript中构建自己的语言。Redux-saga一开始可能觉得有点难学,因为你需要对生成器有基本的了解,但也要理解Redux-saga提供的语言。

我将在这里尝试描述我在redux-saga之上构建的通知系统。这个例子目前正在生产中运行。

高级通知系统规范

  • 您可以请求显示通知
  • 您可以请求通知隐藏
  • 显示通知不应超过4秒
  • 可以同时显示多个通知
  • 不能同时显示超过3个通知
  • 如果在已经有3个显示通知的情况下请求通知,则将其排队/推迟。

结果

我的生产应用程序的屏幕截图Stample.co

烤面包

代码

在这里,我将通知命名为toast,但这是一个命名细节。

function* toastSaga() {
// Some config constantsconst 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 reallylet pendingToasts = []; // A queue of toasts waiting to be displayedlet activeToasts = []; // Toasts currently displayed

// Trigger the display of a toast for 4 secondsfunction* 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 toastsyield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)yield call(delay,ToastDisplayTime); // Wait 4 secondsyield put(events.toastHidden(toast)); // Hide the toastactiveToasts = _.without(activeToasts,toast); // Remove from active toasts}
// Everytime we receive a toast display request, we put that request in the queuefunction* toastRequestsWatcher() {while ( true ) {// Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatchedconst 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 queueconst [firstToast,...remainingToasts] = pendingToasts;pendingToasts = remainingToasts;// Fork means we are creating a subprocess that will handle the display of a single toastyield fork(displayToast,firstToast);// Add little delay so that 2 concurrent toast requests aren't display at the same timeyield 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)]}

减速机:

const reducer = (state = [],event) => {switch (event.name) {case Names.TOAST_DISPLAYED:return [...state,event.data.toastData];case Names.TOAST_HIDDEN:return _.without(state,event.data.toastData);default:return state;}};

用法

您可以简单地调度TOAST_DISPLAY_REQUESTED事件。如果您调度4个请求,只会显示3个通知,一旦第一个通知消失,第4个将在稍后出现。

请注意,我并不特别推荐从JSX调度TOAST_DISPLAY_REQUESTED。您宁愿添加另一个saga来侦听您已经存在的应用程序事件,然后调度TOAST_DISPLAY_REQUESTED:您触发通知的组件不必紧密耦合到通知系统。

结论

我的代码并不完美,但在生产环境中运行了几个月,没有错误。Redux-saga和生成器最初有点难,但一旦你理解了它们,这种系统就很容易构建。

甚至很容易实现更复杂的规则,例如:

  • 当太多的通知被“排队”时,为每个通知提供更少的显示时间,以便更快地减少队列大小。
  • 检测窗口大小变化,并相应地更改显示通知的最大数量(例如,桌面=3,手机纵向=2,手机横向=1)

老实说,祝你好运,用thunks正确地实现这种东西。

请注意,您可以对可观察还原执行完全相同的操作,这与redux-saga非常相似。这几乎是相同的,只是生成器和RxJS之间的品味问题。

在尝试了各种流行的方法(动作创造者,thunks,sagas,epics,效果,自定义中间件)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,在React/Redux应用程序中,我应该把业务逻辑放在哪里?

就像这里的讨论一样,我试图对比和比较各种方法。最终,它引导我引入了一个新的库还原逻辑,它从史诗、传奇、自定义中间件中汲取灵感。

它允许您拦截操作以验证、验证、授权,并提供一种执行异步IO的方法。

一些常见的功能可以简单地声明,例如反跳、节流、取消,并且仅使用来自最新请求(获取最新)的响应。redux逻辑包装您的代码,为您提供此功能。

这可以让你随心所欲地实现核心业务逻辑。除非你愿意,否则你不必使用observables或生成器。使用函数和回调、Promise、异步函数(async/wait)等。

执行简单5s通知的代码如下:

const notificationHide = createLogic({// the action type that will trigger this logictype: '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 laterprocess({ getState, action }, dispatch) {setTimeout(() => {dispatch({ type: 'NOTIFICATION_CLEAR' });}, 5000);}});

我在存储库中有一个更高级的通知示例,其工作原理类似于Sebastian Lorber所描述的,您可以将显示限制为N个项目并在排队的任何项目中旋转。Redux逻辑通知示例

我有各种各样的redux逻辑jsfiddle实时示例以及完整示例。我将继续研究文档和示例。

我很想听听你的反馈。

如果您希望对选择性操作进行超时处理,您可以尝试中间件方法。我在选择性地处理基于Promise的操作时遇到了类似的问题,这个解决方案更灵活。

假设你的动作创造者看起来像这样:

//action creatorbuildAction = (actionData) => ({...actionData,timeout: 500})

超时可以在上面的操作中保存多个值

  • 以ms为单位的数字-用于特定的超时持续时间
  • true-对于恒定的超时持续时间。(在中间件中处理)
  • 未定义-用于立即调度

你的中间件实现看起来像这样:

//timeoutMiddleware.jsconst timeoutMiddleware = store => next => action => {
//If your action doesn't have any timeout attribute, fallback to the default handlerif(!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)}

您现在可以使用redux通过这个中间件层路由所有操作。

createStore(reducer, applyMiddleware(timeoutMiddleware))

你可以找到一些类似的例子这里

包含示例项目的存储库

目前有四个样本项目:

  1. 内联编写异步代码
  2. 提取异步动作创建者
  3. 使用Redux Thunk
  4. 使用Redux佐贺

接受的答案是惊人的。

但是有些东西不见了:

  1. 没有可运行的示例项目,只有一些代码片段。
  2. 没有其他替代方案的示例代码,例如:
    1. Redux Saga

所以我创建了你好异步存储库来添加缺失的东西:

  1. 可运行的项目。您可以下载并运行它们而无需修改。
  2. 提供更多替代方案的示例代码:

Redux Saga

接受的答案已经提供了Async Code Inline、Async Action Generator和Redux Thunk的示例代码片段。为了完整性,我提供了Redux Saga的代码片段:

// actions.js
export const showNotification = (id, text) => {return { type: 'SHOW_NOTIFICATION', id, text }}
export const hideNotification = (id) => {return { type: 'HIDE_NOTIFICATION', id }}
export const showNotificationWithTimeout = (text) => {return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }}

行动简单而纯粹。

// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(mapStateToProps,{ showNotificationWithTimeout })(MyComponent)

组件没有什么特别的。

// sagas.js
import { takeEvery, delay } from 'redux-saga'import { put } from 'redux-saga/effects'import { showNotification, hideNotification } from './actions'
// Worker sagalet nextNotificationId = 0function* showNotificationWithTimeout (action) {const id = nextNotificationId++yield put(showNotification(id, action.text))yield delay(5000)yield put(hideNotification(id))}
// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'function* notificationSaga () {yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)}
export default notificationSaga

沙加基于ES6发电机

// index.js
import createSagaMiddleware from 'redux-saga'import saga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer,applyMiddleware(sagaMiddleware))
sagaMiddleware.run(saga)

与Redux Thunk相比

优点

  • 你不会在回调地狱结束。
  • 您可以轻松测试异步流。
  • 你的行为保持纯洁。

缺点

  • 这取决于相对较新的ES6发电机。

如果上面的代码片段没有回答您的所有问题,请参考可跑项目

正确的方法是使用ReduxThunk,这是一个Redux的流行中间件,根据Redux Thunk留档:

"Redux Thunk中间件允许您编写操作创建者返回一个函数而不是一个动作。thank可用于延迟动作的发出,或仅在特定条件下发出满足。内部函数接收存储方法调度和getState作为参数”。

所以基本上它返回一个函数,你可以延迟调度或将其置于条件状态。

所以像这样的东西会为你做这项工作:

import ReduxThunk from 'redux-thunk';
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {return {type: INCREMENT_COUNTER};}
function incrementAsync() {return dispatch => {setTimeout(() => {// Yay! Can invoke sync or async actions with `dispatch`dispatch(increment());}, 5000);};}

我知道这个问题有点老,但我将使用可观察还原来介绍另一个解决方案。Epic。

引用官方留档:

什么是Redux-observable?

基于RxJS 5的Redux中间件。编写和取消异步操作以产生副作用和更多。

Epic是redux-observable的核心原语。

它是一个接受动作流并返回流的函数

动作输入,动作输出

或多或少,您可以创建一个通过Stream接收操作的函数,然后返回一个新的操作流(使用常见的副作用,如超时、延迟、间隔和请求)。

让我发布代码,然后详细解释一下。

store.js

import {createStore, applyMiddleware} from 'redux'import {createEpicMiddleware} from 'redux-observable'import {Observable} from 'rxjs'const NEW_NOTIFICATION = 'NEW_NOTIFICATION'const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'const NOTIFICATION_TIMEOUT = 2000
const initialState = ''const rootReducer = (state = initialState, action) => {const {type, message} = actionconsole.log(type)switch(type) {case NEW_NOTIFICATION:return messagebreakcase QUIT_NOTIFICATION:return initialStatebreak}
return state}
const rootEpic = (action$) => {const incoming = action$.ofType(NEW_NOTIFICATION)const outgoing = incoming.switchMap((action) => {return Observable.of(quitNotification()).delay(NOTIFICATION_TIMEOUT)//.takeUntil(action$.ofType(NEW_NOTIFICATION))});
return outgoing;}
export function newNotification(message) {return ({type: NEW_NOTIFICATION, message})}export function quitNotification(message) {return ({type: QUIT_NOTIFICATION, message});}
export const configureStore = () => createStore(rootReducer,applyMiddleware(createEpicMiddleware(rootEpic)))

index.js

import React from 'react';import ReactDOM from 'react-dom';import App from './App';import {configureStore} from './store.js'import {Provider} from 'react-redux'
const store = configureStore()
ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root'));

App.js

import React, { Component } from 'react';import {connect} from 'react-redux'import {newNotification} from './store.js'
class App extends Component {
render() {return (<div className="App">{this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}<button onClick={this.props.onNotificationRequest}>Click!</button></div>);}}
const mapStateToProps = (state) => {return {notificationExistance : state.length > 0,notificationMessage : state}}
const mapDispatchToProps = (dispatch) => {return {onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))}}
export default connect(mapStateToProps, mapDispatchToProps)(App)

解决这个问题的关键代码就像你看到的馅饼一样简单,唯一与其他答案不同的是函数rootEpic。

点1。与sagas一样,你必须组合epics才能获得一个接收动作流并返回动作流的顶级函数,所以你可以将它与中间件工厂创建中间件一起使用。在我们的例子中,我们只需要一个,所以我们只有rootEpic,所以我们不必组合任何东西,但这是一个很好的事实。

点2。我们的rootEpic关心副作用逻辑只需要大约5行代码,这太棒了!包括几乎是声明性的事实!

第3点。逐行rootEpic解释(在评论中)

const rootEpic = (action$) => {// sets the incoming constant as a stream// of actions with  type NEW_NOTIFICATIONconst 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 alertconst 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 streamreturn outgoing;}

希望有帮助!

Redux本身是一个非常冗长的库,对于此类内容,您必须使用类似Redux-thunk的东西,它将提供dispatch函数,因此您将能够在几秒钟后调度通知的关闭。

我创建了一个图书馆来解决冗长性和可组合性等问题,您的示例如下所示:

import { createTile, createSyncTile } from 'redux-tiles';import { sleep } from 'delounce';
const notifications = createSyncTile({type: ['ui', 'notifications'],fn: ({ params }) => params.data,// to have only one tile for all notificationsnesting: ({ type }) => [type],});
const notificationsManager = createTile({type: ['ui', 'notificationManager'],fn: ({ params, dispatch, actions }) => {dispatch(actions.ui.notifications({ type: params.type, data: params.data }));await sleep(params.timeout || 5000);dispatch(actions.ui.notifications({ type: params.type, data: null }));return { closed: true };},nesting: ({ type }) => [type],});

因此,我们编写同步操作来显示异步操作中的通知,它可以请求一些背景信息,或者稍后检查通知是否手动关闭。

为什么要这么难?只是UI逻辑。使用一个专用的操作来设置通知数据:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

和一个专门的组件来显示它:

const Notifications = ({ notificationData }) => {if(notificationData.expire > this.state.currentTime) {return <div>{notificationData.message}</div>} else return null;}

在这种情况下,问题应该是“如何清理旧状态?”,“如何通知组件时间已更改”

您可以在组件的setTimeout上实现一些TIMEOUT操作。

也许只要显示新通知就可以清理它。

无论如何,应该有一些setTimeout的地方,对吧?为什么不在组件中做呢

setTimeout(() => this.setState({ currentTime: +new Date()}),this.props.notificationData.expire-(+new Date()) )

其动机是“通知淡出”功能实际上是一个UI问题。因此,它简化了业务逻辑的测试。

测试它是如何实现的似乎没有意义。只有验证通知何时应该超时才有意义。因此,更少的代码存根、更快的测试、更干净的代码。

很简单。使用trim-redux包并在componentDidMount或其他地方这样写,然后在componentWillUnmount中杀死它。

componentDidMount() {this.tm = setTimeout(function() {setStore({ age: 20 });}, 3000);}
componentWillUnmount() {clearTimeout(this.tm);}

这可能有点偏离主题,但我想在这里分享它,因为我只是想在给定的超时后从状态中删除警报,即自动隐藏警报/通知。

我最终在<Alert />组件中使用了setTimeout(),这样它就可以在给定的id上调用和调度REMOVE操作。

export function Alert(props: Props) {useEffect(() => {const timeoutID = setTimeout(() => {dispatchAction({type: REMOVE,payload: {id: id,},});}, timeout ?? 2000);return () => clearTimeout(timeoutID);}, []);return <AlertComponent {...props} />;}

Redux操作只能返回普通物体,而不是函数、回调或异步进程。为了通过Web API(例如timeout()方法)调度它们,您必须使用Redux-thunk中间件。它是为处理这样的过程而创建的。

  1. 第一个配置redux-thunk通过留档redux-thunk
  2. 其次,以这种方式更改您的动作创建者:
const yourAction = millisecond => dispatch => {setTimeout(() => {dispatch({type: 'YOUR_ACTIION_TYPE',payload: yourWhatEverPayload})}, millisecond)}