如何使用超时调度Redux操作?

本文翻译自:How to dispatch a Redux action with a timeout?

I have an action that updates notification state of my application. 我有一个更新我的应用程序的通知状态的操作。 Usually, this notification will be an error or info of some sort. 通常,此通知将是某种错误或信息。 I need to then dispatch another action after 5 seconds that will return the notification state to initial one, so no notification. 我需要在5秒后发送另一个操作,将通知状态返回到初始状态,因此不会发出通知。 The main reason behind this is to provide functionality where notifications disappear automatically after 5 seconds. 这背后的主要原因是提供通知在5秒后自动消失的功能。

I had no luck with using setTimeout and returning another action and can't find how this is done online. 我没有运气使用setTimeout并返回另一个动作,但无法找到如何在线完成。 So any advice is welcome. 所以欢迎任何建议。


#1楼

参考:https://stackoom.com/question/2Oa7z/如何使用超时调度Redux操作


#2楼

You can do this with redux-thunk . 你可以用redux-thunk做到这一点。 There is a guide in redux document for async actions like setTimeout. redux文档中有一个指南,用于异步操作,如setTimeout。


#3楼

Don't fall into the trap of thinking a library should prescribe how to do everything . 不要陷入认为图书馆应该规定如何做所有事情的陷阱 。 If you want to do something with a timeout in JavaScript, you need to use setTimeout . 如果您想在JavaScript中执行超时操作,则需要使用setTimeout There is no reason why Redux actions should be any different. 没有理由认为Redux的行为应该有所不同。

Redux does offer some alternative ways of dealing with asynchronous stuff, but you should only use those when you realize you are repeating too much code. Redux 确实提供了一些处理异步内容的替代方法,但是只有在意识到重复代码太多时才应该使用它们。 Unless you have this problem, use what the language offers and go for the simplest solution. 除非您遇到此问题,否则请使用语言提供的内容并选择最简单的解决方案。

Writing Async Code Inline 编写异步代码内联

This is by far the simplest way. 这是迄今为止最简单的方法。 And there's nothing specific to Redux here. 这里没有Redux特有的东西。

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

Similarly, from inside a connected component: 同样,从连接组件内部:

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

The only difference is that in a connected component you usually don't have access to the store itself, but get either dispatch() or specific action creators injected as props. 唯一的区别是,在连接组件中,您通常无法访问商店本身,但可以将dispatch()或特定操作创建者注入为道具。 However this doesn't make any difference for us. 然而,这对我们没有任何影响。

If you don't like making typos when dispatching the same actions from different components, you might want to extract action creators instead of dispatching action objects inline: 如果您不喜欢在从不同组件调度相同操作时输入拼写错误,则可能需要提取操作创建器,而不是内联调度操作对象:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

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

Or, if you have previously bound them with connect() : 或者,如果您之前使用connect()绑定它们:

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

So far we have not used any middleware or other advanced concept. 到目前为止,我们还没有使用任何中间件或其他高级概念。

Extracting Async Action Creator 提取异步动作创建器

The approach above works fine in simple cases but you might find that it has a few problems: 上述方法在简单的情况下工作正常,但您可能会发现它有一些问题:

  • It forces you to duplicate this logic anywhere you want to show a notification. 它会强制您在要显示通知的任何位置复制此逻辑。
  • The notifications have no IDs so you'll have a race condition if you show two notifications fast enough. 通知没有ID,因此如果您足够快地显示两个通知,则会出现竞争条件。 When the first timeout finishes, it will dispatch HIDE_NOTIFICATION , erroneously hiding the second notification sooner than after the timeout. 当第一个超时完成时,它将调度HIDE_NOTIFICATION ,错误地比超时后错误地隐藏第二个通知。

To solve these problems, you would need to extract a function that centralizes the timeout logic and dispatches those two actions. 要解决这些问题,您需要提取一个集中超时逻辑并调度这两个操作的函数。 It might look like this: 它可能看起来像这样:

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

Now components can use showNotificationWithTimeout without duplicating this logic or having race conditions with different notifications: 现在组件可以使用showNotificationWithTimeout而不复制此逻辑或具有不同通知的竞争条件:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Why does showNotificationWithTimeout() accept dispatch as the first argument? 为什么showNotificationWithTimeout()接受dispatch作为第一个参数? Because it needs to dispatch actions to the store. 因为它需要将操作分派给商店。 Normally a component has access to dispatch but since we want an external function to take control over dispatching, we need to give it control over dispatching. 通常,组件可以访问dispatch但由于我们希望外部函数控制调度,因此我们需要控制调度。

If you had a singleton store exported from some module, you could just import it and dispatch directly on it instead: 如果您有从单个模块导出的单件商店,您只需导入它并直接在其上dispatch

// 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.')    

This looks simpler but we don't recommend this approach . 这看起来更简单,但我们不建议采用这种方法 The main reason we dislike it is because it forces store to be a singleton . 我们不喜欢它的主要原因是因为它迫使商店成为单身人士 This makes it very hard to implement server rendering . 这使得实现服务器呈现非常困难。 On the server, you will want each request to have its own store, so that different users get different preloaded data. 在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。

A singleton store also makes testing harder. 单身商店也使测试更加困难。 You can no longer mock a store when testing action creators because they reference a specific real store exported from a specific module. 在测试动作创建者时,您不能再模拟商店,因为它们引用从特定模块导出的特定实体商店。 You can't even reset its state from outside. 你甚至无法从外面重置它的状态。

So while you technically can export a singleton store from a module, we discourage it. 因此,虽然您在技术上可以从模块导出单件商店,但我们不鼓励它。 Don't do this unless you are sure that your app will never add server rendering. 除非您确定您的应用永远不会添加服务器渲染,否则不要这样做。

Getting back to the previous version: 回到以前的版本:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

This solves the problems with duplication of logic and saves us from race conditions. 这解决了重复逻辑的问题,并使我们免于竞争条件。

Thunk Middleware Thunk中间件

For simple apps, the approach should suffice. 对于简单的应用程序,该方法应该足够了。 Don't worry about middleware if you're happy with it. 如果您对它感到满意,请不要担心中间件。

In larger apps, however, you might find certain inconveniences around it. 但是,在较大的应用中,您可能会发现一些不便之处。

For example, it seems unfortunate that we have to pass dispatch around. 例如,我们不得不通过dispatch似乎很不幸。 This makes it trickier to separate container and presentational components because any component that dispatches Redux actions asynchronously in the manner above has to accept dispatch as a prop so it can pass it further. 这使得分离容器和表示组件变得更加棘手,因为以上述方式异步调度Redux操作的任何组件都必须接受dispatch作为prop,以便它可以进一步传递它。 You can't just bind action creators with connect() anymore because showNotificationWithTimeout() is not really an action creator. 你不能再仅仅使用connect()绑定动作创建者,因为showNotificationWithTimeout()实际上不是动作创建者。 It does not return a Redux action. 它不会返回Redux操作。

In addition, it can be awkward to remember which functions are synchronous action creators like showNotification() and which are asynchronous helpers like showNotificationWithTimeout() . 另外,记住哪些函数是showNotification()等同步动作创建者以及showNotification()类的异步助手可能showNotificationWithTimeout() You have to use them differently and be careful not to mistake them with each other. 你必须以不同的方式使用它们,并注意不要互相误解。

This was the motivation for 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 rather than totally different functions. 这是寻找一种方法来“合理化”这种向辅助函数提供dispatch的方式的动机,并帮助Redux“看到”这样的异步动作创建者作为正常动作创建者的特例而不是完全不同的函数。

If you're still with us and you also recognize as a problem in your app, you are welcome to use the Redux Thunk middleware. 如果您仍然和我们在一起,并且您也认为应用程序存在问题,欢迎您使用Redux Thunk中间件。

In a gist, Redux Thunk teaches Redux to recognize special kinds of actions that are in fact functions: 在一个要点中,Redux Thunk教Redux识别实际上具有功能的特殊动作:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

When this middleware is enabled, if you dispatch a function , Redux Thunk middleware will give it dispatch as an argument. 当启用这个中间件, 如果你发送的功能 ,终极版咚中间件将会给它dispatch作为参数。 It will also “swallow” such actions so don't worry about your reducers receiving weird function arguments. 它也会“吞下”这样的动作,所以不要担心你的减速器接收奇怪的函数参数。 Your reducers will only receive plain object actions—either emitted directly, or emitted by the functions as we just described. 您的Reducer将只接收普通对象操作 - 直接发出,或者由我们刚刚描述的函数发出。

This does not look very useful, does it? 这看起来不是很有用,是吗? Not in this particular situation. 不是在这种特殊情况下。 However it lets us declare showNotificationWithTimeout() as a regular Redux action creator: 但是它允许我们将showNotificationWithTimeout()声明为常规的Redux动作创建者:

// 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(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

Note how the function is almost identical to the one we wrote in the previous section. 请注意该函数与我们在上一节中编写的函数几乎完全相同。 However it doesn't accept dispatch as the first argument. 但是它不接受dispatch作为第一个参数。 Instead it returns a function that accepts dispatch as the first argument. 相反,它返回一个接受dispatch作为第一个参数的函数。

How would we use it in our component? 我们如何在我们的组件中使用它? Definitely, we could write this: 当然,我们可以这样写:

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

We are calling the async action creator to get the inner function that wants just dispatch , and then we pass dispatch . 我们正在调用异步操作创建器来获取只需要dispatch的内部函数,然后我们通过dispatch

However this is even more awkward than the original version! 然而,这比原始版本更加尴尬! Why did we even go that way? 为什么我们甚至走那条路?

Because of what I told you before. 因为我之前告诉你的。 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中间件,则只要您尝试调度函数而不是操作对象,中间件就会使用dispatch方法本身作为第一个参数来调用该函数

So we can do this instead: 所以我们可以这样做:

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

Finally, dispatching an asynchronous action (really, a series of actions) looks no different than dispatching a single action synchronously to the component. 最后,调度异步操作(实际上是一系列操作)与将同一个操作同步分派给组件没有什么不同。 Which is good because components shouldn't care whether something happens synchronously or asynchronously. 这是好事,因为组件不应该关心某些事情是同步发生还是异步发生。 We just abstracted that away. 我们只是把它抽象出来了。

Notice that since we “taught” Redux to recognize such “special” action creators (we call them thunk action creators), we can now use them in any place where we would use regular action creators. 请注意,由于我们“教导”Redux识别出这样的“特殊”动作创建者(我们称之为thunk动作创建者),我们现在可以在任何我们使用常规动作创建者的地方使用它们。 For example, we can use them with connect() : 例如,我们可以将它们与connect()

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

Reading State in Thunks 在Thunks中阅读状态

Usually your reducers contain the business logic for determining the next state. 通常,您的Reducer包含用于确定下一个状态的业务逻辑。 However, reducers only kick in after the actions are dispatched. 但是,只有在调度动作后才会启动减速器。 What if you have a side effect (such as calling an API) in a thunk action creator, and you want to prevent it under some condition? 如果您在thunk动作创建者中有副作用(例如调用API),并且您想在某些条件下阻止它,该怎么办?

Without using the thunk middleware, you'd just do this check inside the component: 不使用thunk中间件,你只需在组件内部进行检查:

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

However, the point of extracting an action creator was to centralize this repetitive logic across many components. 但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件上。 Fortunately, Redux Thunk offers you a way to read the current state of the Redux store. 幸运的是,Redux Thunk为您提供了一种读取 Redux商店当前状态的方法。 In addition to dispatch , it also passes getState as the second argument to the function you return from your thunk action creator. 除了dispatch ,它还将getState作为第二个参数传递给您从thunk action creator返回的函数。 This lets the thunk read the current state of the store. 这让thunk读取商店的当前状态。

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

Don't abuse this pattern. 不要滥用这种模式。 It is good for bailing out of API calls when there is cached data available, but it is not a very good foundation to build your business logic upon. 当有缓存数据可用时,它可以避免API调用,但它不是构建业务逻辑的良好基础。 If you use getState() only to conditionally dispatch different actions, consider putting the business logic into the reducers instead. 如果仅使用getState()来有条件地分派不同的操作,请考虑将业务逻辑放入reducers中。

Next Steps 下一步

Now that you have a basic intuition about how thunks work, check out Redux async example which uses them. 既然您对thunks如何工作有基本的直觉,请查看使用它们的Redux 异步示例 。

You may find many examples in which thunks return Promises. 你可能会发现许多thunk返回Promises的例子。 This is not required but can be very convenient. 这不是必需的,但可以非常方便。 Redux doesn't care what you return from a thunk, but it gives you its return value from dispatch() . Redux并不关心你从thunk返回什么,但是它会从dispatch()它的返回值。 This is why you can return a Promise from a thunk and wait for it to complete by calling dispatch(someThunkReturningPromise()).then(...) . 这就是为什么你可以从thunk返回一个Promise并等待它通过调用dispatch(someThunkReturningPromise()).then(...)

You may also split complex thunk action creators into several smaller thunk action creators. 您也可以将复杂的thunk动作创建者分成几个较小的thunk动作创建者。 The dispatch method provided by thunks can accept thunks itself, so you can apply the pattern recursively. thunk提供的dispatch方法本身可以接受thunk,因此可以递归地应用该模式。 Again, this works best with Promises because you can implement asynchronous control flow on top of that. 同样,这最适合Promises,因为您可以在其上实现异步控制流。

For some apps, you may find yourself in a situation where your asynchronous control flow requirements are too complex to be expressed with thunks. 对于某些应用程序,您可能会发现自己的异步控制流要求过于复杂而无法用thunk表示。 For example, retrying failed requests, reauthorization flow with tokens, or a step-by-step onboarding can be too verbose and error-prone when written this way. 例如,重试失败的请求,带令牌的重新授权流程或逐步入门可能过于冗长且以这种方式编写时容易出错。 In this case, you might want to look at more advanced asynchronous control flow solutions such as Redux Saga or Redux Loop . 在这种情况下,您可能希望查看更高级的异步控制流解决方案,例如Redux Saga或Redux Loop 。 Evaluate them, compare the examples relevant to your needs, and pick the one you like the most. 评估它们,比较与您的需求相关的示例,并选择您最喜欢的那个。

Finally, don't use anything (including thunks) if you don't have the genuine need for them. 最后,如果你没有真正的需要,不要使用任何东西(包括thunk)。 Remember that, depending on the requirements, your solution might look as simple as 请记住,根据要求,您的解决方案可能看起来很简单

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

Don't sweat it unless you know why you're doing this. 除非你知道为什么要这样做,否则不要出汗。


#4楼

I would recommend also taking a look at the SAM pattern . 我建议也看看SAM模式 。

The SAM pattern advocates for including a "next-action-predicate" where (automatic) actions such as "notifications disappear automatically after 5 seconds" are triggered once the model has been updated (SAM model ~ reducer state + store). SAM模式主张包括“下一个动作谓词”,其中一旦模型更新(SAM模型〜减速器状态+存储),其中(自动)动作(例如“通知在5秒后自动消失”)被触发。

The pattern advocates for sequencing actions and model mutations one at a time, because the "control state" of the model "controls" which actions are enabled and/or automatically executed by the next-action predicate. 该模式主张一次一个地对动作和模型突变进行排序,因为模型的“控制状态”“控制”由下一个动作谓词启用和/或自动执行哪些动作。 You simply cannot predict (in general) what state the system will be prior to processing an action and hence whether your next expected action will be allowed/possible. 您根本无法预测(通常)系统处理操作之前的状态,因此您的下一个预期操作是否允许/可能。

So for instance the code, 所以例如代码,

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

would not be allowed with SAM, because the fact that a hideNotification action can be dispatched is dependent on the model successfully accepting the value "showNotication: true". 不允许使用SAM,因为可以调度hideNotification操作的事实取决于成功接受值“showNotication:true”的模型。 There could be other parts of the model that prevents it from accepting it and therefore, there would be no reason to trigger the hideNotification action. 模型的其他部分可能会阻止它接受它,因此,没有理由触发hideNotification操作。

I would highly recommend that implement a proper next-action predicate after the store updates and the new control state of the model can be known. 我强烈建议在存储更新和模型的新控制状态之后实现适当的下一个动作谓词。 That's the safest way to implement the behavior you are looking for. 这是实现您正在寻找的行为最安全的方式。

You can join us on Gitter if you'd like. 如果您愿意,可以加入我们的Gitter。 There is also a SAM getting started guide available here . 此处还提供了SAM入门指南 。


#5楼

Using Redux-saga 使用Redux-saga

As Dan Abramov said, if you want more advanced control over your async code, you might take a look at redux-saga . 正如Dan Abramov所说,如果你想对异步代码进行更高级的控制,你可以看看redux-saga 。

This answer is a simple example, if you want better explanations on why redux-saga can be useful for your application, check this other answer . 这个答案是一个简单的例子,如果你想更好地解释为什么redux-saga对你的应用程序有用,请查看其他答案 。

The general idea is that Redux-saga offers an ES6 generators interpreter that permits you to easily write async code that looks like synchronous code (this is why you'll often find infinite while loops in Redux-saga). 一般的想法是Redux-saga提供了一个ES6生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么你经常在Redux-saga中找到无限的while循环)。 Somehow, Redux-saga is building its own language directly inside Javascript. 不知何故,Redux-saga正在Javascript中直接构建自己的语言。 Redux-saga can feel a bit difficult to learn at first, because you need basic understanding of generators, but also understand the language offered by Redux-saga. Redux-saga起初可能感觉有点难学,因为你需要对生成器有基本的了解,但也要理解Redux-saga提供的语言。

I'll try here to describe here the notification system I built on top of redux-saga. 我将在这里尝试描述我在redux-saga之上构建的通知系统。 This example currently runs in production. 此示例目前在生产中运行。

Advanced notification system specification 高级通知系统规范

  • You can request a notification to be displayed 您可以请求显示通知
  • You can request a notification to hide 您可以请求隐藏通知
  • A notification should not be displayed more than 4 seconds 通知不应超过4秒
  • Multiple notifications can be displayed at the same time 可以同时显示多个通知
  • No more than 3 notifications can be displayed at the same time 可以同时显示不超过3个通知
  • If a notification is requested while there are already 3 displayed notifications, then queue/postpone it. 如果在已经显示3个通知的情况下请求通知,则排队/推迟通知。

Result 结果

Screenshot of my production app Stample.co 我的生产应用程序Stample.co的屏幕截图

Code

Here I named the notification a toast but this is a naming detail. 在这里,我将通知命名为toast但这是一个命名细节。

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

And the reducer: 还原剂:

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

Usage 用法

You can simply dispatch TOAST_DISPLAY_REQUESTED events. 您只需调度TOAST_DISPLAY_REQUESTED事件即可。 If you dispatch 4 requests, only 3 notifications will be displayed, and the 4th one will appear a bit later once the 1st notification disappears. 如果您发送4个请求,则只会显示3个通知,第1个通知消失后,第4个通知会稍后显示。

Note that I don't specifically recommend dispatching TOAST_DISPLAY_REQUESTED from JSX. 请注意,我不特别建议从JSX调度TOAST_DISPLAY_REQUESTED You'd rather add another saga that listens to your already-existing app events, and then dispatch the TOAST_DISPLAY_REQUESTED : your component that triggers the notification, does not have to be tightly coupled to the notification system. 您宁愿添加另一个听取您已经存在的应用事件的传奇,然后调度TOAST_DISPLAY_REQUESTED :触发​​通知的组件,不必与通知系统紧密耦合。

Conclusion 结论

My code is not perfect but runs in production with 0 bugs for months. 我的代码并不完美,但在生产中运行了0个bug几个月。 Redux-saga and generators are a bit hard initially but once you understand them this kind of system is pretty easy to build. Redux-saga和生成器最初有点难,但是一旦你理解它们,这种系统很容易构建。

It's even quite easy to implement more complex rules, like: 实现更复杂的规则甚至很容易,例如:

  • when too many notifications are "queued", give less display-time for each notification so that the queue size can decrease faster. 当太多通知被“排队”时,为每个通知提供更少的显示时间,以便更快地减少队列大小。
  • detect window size changes, and change the maximum number of displayed notifications accordingly (for example, desktop=3, phone portrait = 2, phone landscape = 1) 检测窗口大小更改,并相应地更改显示的通知的最大数量(例如,桌面= 3,手机肖像= 2,手机格局= 1)

Honnestly, good luck implementing this kind of stuff properly with thunks. 恭顺,祝你好运用thunks实现这种东西。

Note you can do exactly the same kind of thing with redux-observable which is very similar to redux-saga. 请注意,你可以使用redux-observable做同样的事情,这与redux-saga非常相似。 It's almost the same and is a matter of taste between generators and RxJS. 它几乎是相同的,是发电机和RxJS之间的品味问题。


#6楼

After trying the various popular approaches (action creators, thunks, sagas, epics, effects, custom middleware), I still felt that maybe there was room for improvement so I documented my journey in this blog article, Where do I put my business logic in a React/Redux application? 在尝试各种流行的方法(动作创作者,thunk,sagas,史诗,效果,自定义中间件)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,我在哪里放置我的业务逻辑一个React / Redux应用程序?

Much like the discussions here, I tried to contrast and compare the various approaches. 就像这里的讨论一样,我试图对比并比较各种方法。 Eventually it led me to introducing a new library redux-logic which takes inspiration from epics, sagas, custom middleware. 最终,它让我引入了一个新的图书馆redux-logic ,它从史诗,传奇,自定义中间件中获取灵感。

It allows you to intercept actions to validate, verify, authorize, as well as providing a way to perform async IO. 它允许您拦截操作以验证,验证,授权以及提供执行异步IO的方法。

Some common functionality can simply be declared like debouncing, throttling, cancellation, and only using the response from the latest request (takeLatest). 一些常见功能可以简单地声明为去抖动,限制,取消,并且仅使用来自最新请求(takeLatest)的响应。 redux-logic wraps your code providing this functionality for you. redux-logic包装您的代码,为您提供此功能。

That frees you to implement your core business logic however you like. 这使您可以随心所欲地实现核心业务逻辑。 You don't have to use observables or generators unless you want to. 除非您愿意,否则不必使用可观察量或生成器。 Use functions and callbacks, promises, async functions (async/await), etc. 使用函数和回调,promises,异步函数(async / await)等。

The code for doing a simple 5s notification would be something like: 做一个简单的5s通知的代码是这样的:

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

I have a more advanced notification example in my repo that works similar to what Sebastian Lorber described where you could limit the display to N items and rotate through any that queued up. 我在我的仓库中有一个更高级的通知示例,其工作方式类似于Sebastian Lorber所描述的,您可以将显示限制为N个项目并旋转排队的任何项目。 redux-logic notification example redux-logic通知示例

I have a variety of redux-logic jsfiddle live examples as well as full examples . 我有各种redux-logic jsfiddle实例和完整的例子 。 I'm continuing to work on docs and examples. 我将继续研究文档和示例。

I'd love to hear your feedback. 我很想听听你的反馈。

你可能感兴趣的:(如何使用超时调度Redux操作?)