如何在超时时分派 Redux 操作? [英] How to dispatch a Redux action with a timeout?

查看:22
本文介绍了如何在超时时分派 Redux 操作?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have an action that updates the 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 the initial one, so no notification. The main reason behind this is to provide functionality where notifications disappear automatically after 5 seconds.

我在使用 setTimeout 并返回另一个操作时运气不佳,但找不到在线完成此操作的方式.因此,欢迎提出任何建议.

I had no luck with using setTimeout and returning another action and can't find how this is done online. So any advice is welcome.

推荐答案

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

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. 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. Unless you have this problem, use what the language offers and go for the simplest solution.

这是迄今为止最简单的方法.而且这里没有任何特定于 Redux 的内容.

This is by far the simplest way. And there’s nothing specific to Redux here.

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)

唯一的区别是,在连接组件中,您通常无法访问 store 本身,但可以获取 dispatch() 或作为 props 注入的特定动作创建者.然而,这对我们没有任何影响.

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

或者,如果你之前用 connect() 绑定过它们:

Or, if you have previously bound them with 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.

上述方法在简单的情况下工作正常,但您可能会发现它有一些问题:

The approach above works fine in simple cases but you might find that it has a few problems:

  • 它迫使您在任何想要显示通知的地方复制此逻辑.
  • 通知没有 ID,因此如果您足够快地显示两个通知,就会出现竞争条件.当第一个超时完成时,它会调度 HIDE_NOTIFICATION,错误地比超时后更早地隐藏第二个通知.
  • 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. When the first timeout finishes, it will dispatch HIDE_NOTIFICATION, erroneously hiding the second notification sooner than after the timeout.

要解决这些问题,您需要提取一个函数来集中超时逻辑并分派这两个操作.它可能看起来像这样:

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

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

Now components can use showNotificationWithTimeout without duplicating this logic or having race conditions with different notifications:

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

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

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

Why does showNotificationWithTimeout() accept dispatch as the first argument? 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:

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

回到之前的版本:

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

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

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.

例如,我们不得不传递dispatch似乎很不幸.这使得分离容器和展示组件变得更加棘手,因为任何组件以上述方式异步调度 Redux 操作必须接受 dispatch 作为 prop,以便它可以进一步传递它.你不能再用 connect() 绑定动作创建者,因为 showNotificationWithTimeout() 并不是真正的动作创建者.它不返回 Redux 操作.

For example, it seems unfortunate that we have to pass dispatch around. 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. You can’t just bind action creators with connect() anymore because showNotificationWithTimeout() is not really an action creator. It does not return a Redux action.

此外,记住哪些函数是像 showNotification() 这样的同步动作创建者,哪些是像 showNotificationWithTimeout() 这样的异步辅助函数,可能会很尴尬.您必须以不同的方式使用它们,并注意不要将它们混淆.

In addition, it can be awkward to remember which functions are synchronous action creators like showNotification() and which are asynchronous helpers like showNotificationWithTimeout(). You have to use them differently and be careful not to mistake them with each other.

这是找到一种方法使这种向辅助函数提供dispatch的模式合法化"的动机,并帮助Redux看到"这样的异步动作创建者作为特例正常的动作创建者,而不是完全不同的功能.

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.

如果您仍然与我们在一起并且您也认为您的应用程序存在问题,欢迎您使用 Redux Thunk 中间件.

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 教 Redux 识别实际上是函数的特殊类型的操作:

In a gist, Redux Thunk teaches Redux to recognize special kinds of actions that are in fact functions:

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

当这个中间件被启用时,如果你调度一个函数,Redux Thunk中间件会给它dispatch作为参数.它也会吞下"这样的动作,所以不用担心你的 reducer 会收到奇怪的函数参数.您的 reducer 将只接收普通对象操作——直接发出,或由我们刚刚描述的函数发出.

When this middleware is enabled, if you dispatch a function, Redux Thunk middleware will give it dispatch as an argument. 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.

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

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:

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

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

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. Instead it returns a function that accepts dispatch as the first argument.

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

How would we use it in our component? Definitely, we could write this:

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

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

We are calling the async action creator to get the inner function that wants just dispatch, and then we pass dispatch.

不过这比原版还要别扭!我们为什么要走那条路?

However this is even more awkward than the original version! Why did we even go that way?

因为我之前告诉过你.如果启用了 Redux Thunk 中间件,则任何时候您尝试调度函数而不是操作对象时,中间件都会调用该函数,并将 dispatch 方法本身作为第一个参数.

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.

所以我们可以这样做:

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

请注意,由于我们教"了 Redux 来识别此类特殊"动作创建者(我们称他们为 thunk 动作创建器),我们现在可以在任何需要使用常规动作创建器的地方使用它们.例如,我们可以将它们与 connect() 一起使用:

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. For example, we can use them with 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)

Thunks 中的读取状态

通常您的减速器包含用于确定下一个状态的业务逻辑.然而,reducer 只在动作被分派后才开始.如果您在 thunk 动作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它怎么办?

Reading State in Thunks

Usually your reducers contain the business logic for determining the next state. 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 中间件,您只需在组件内部执行此检查:

Without using the thunk middleware, you’d just do this check inside the component:

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

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

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. In addition to dispatch, it also passes getState as the second argument to the function you return from your thunk action creator. This lets the thunk read the current state of the store.

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

不要滥用这种模式.当有可用的缓存数据时,它有助于避免 API 调用,但它不是构建业务逻辑的良好基础.如果您仅使用 getState() 来有条件地分派不同的操作,请考虑将业务逻辑放入 reducer.

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. If you use getState() only to conditionally dispatch different actions, consider putting the business logic into the reducers instead.

既然您对 thunk 的工作原理有了基本的了解,请查看 Redux 异步示例 使用它们.

Now that you have a basic intuition about how thunks work, check out Redux async example which uses them.

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

You may find many examples in which thunks return 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(). This is why you can return a Promise from a thunk and wait for it to complete by calling dispatch(someThunkReturningPromise()).then(...).

您还可以将复杂的 thunk 动作创建器拆分为几个较小的 thunk 动作创建器.thunks 提供的 dispatch 方法可以接受 thunks 本身,因此您可以递归地应用该模式.同样,这对 Promise 最有效,因为您可以在此基础上实现异步控制流.

You may also split complex thunk action creators into several smaller thunk action creators. The dispatch method provided by thunks can accept thunks itself, so you can apply the pattern recursively. Again, this works best with Promises because you can implement asynchronous control flow on top of that.

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

For some apps, you may find yourself in a situation where your asynchronous control flow requirements are too complex to be expressed with thunks. 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. Evaluate them, compare the examples relevant to your needs, and pick the one you like the most.

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

Finally, don’t use anything (including thunks) if you don’t have the genuine need for them. 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.

这篇关于如何在超时时分派 Redux 操作?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆