小编典典

如何在超时的情况下调度 Redux 操作?

all

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

我没有使用setTimeout和返回另一个动作的运气,并且无法找到如何在线完成。所以欢迎任何建议。


阅读 214

收藏
2022-02-28

共1个答案

小编典典

不要落入认为图书馆应该规定如何做所有事情的陷阱。如果你想在 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.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()

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

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

提取异步动作创建者

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

  • 它迫使您在要显示通知的任何地方复制此逻辑。
  • 通知没有 ID,因此如果您足够快地显示两个通知,您将面临竞争条件。当第一个超时结束时,它将派发HIDE_NOTIFICATION,错误地隐藏第二个通知,而不是在超时之后。

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

// 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鈥檇 still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

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

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

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

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

为什么showNotificationWithTimeout()接受dispatch作为第一个参数?因为它需要向 store
发送操作。通常一个组件可以访问,dispatch但由于我们想要一个外部函数来控制调度,我们需要让它控制调度。

如果你有一个从某个模块导出的单例存储,你可以直接导入它并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.')

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

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

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

回到以前的版本:

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

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

Thunk 中间件

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

但是,在较大的应用程序中,您可能会发现一些不便之处。

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

此外,记住哪些函数是同步动作创建者showNotification(),哪些是异步助手,可能会很尴尬showNotificationWithTimeout()。您必须以不同的方式使用它们,并注意不要将它们误认为是彼此。

这就是 寻找一种方法来“合法化”这种dispatch向辅助函数提供模式的方法的动机,并帮助 Redux
“ee”这样的异步动作创建者作为普通动作创建者的特例,
而不是完全不同的函数。

如果您仍然和我们在一起,并且您还发现您的应用程序存在问题,那么欢迎您使用Redux
Thunk
中间件。

概括地说,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)
})

启用此中间件后, 如果您 dispatch 一个函数 ,Redux Thunk
中间件会将其dispatch作为参数提供。它也会“沉迷”这样的动作,所以不要担心你的 reducer 会收到奇怪的函数参数。你的 reducer
只会接收普通的对象动作——无论是直接发出的,还是由我们刚刚描述的函数发出的。

这看起来不是很有用,是吗?不是在这种特殊情况下。然而,它让我们声明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)
  }
}

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

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

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

我们正在调用异步操作创建者来获取想要的内部函数,dispatch然后我们通过dispatch.

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

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

所以我们可以这样做:

// component.js
this.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 = 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 包含用于确定下一个状态的业务逻辑。但是,reducer 仅在动作被调度后才开始。如果您在 thunk
动作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它怎么办?

在不使用 thunk 中间件的情况下,您只需在组件内部进行以下检查:

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

但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件中。幸运的是,Redux Thunk 为您提供了一种 读取 Redux
存储当前状态的方法。除了dispatch,它还getState作为第二个参数传递给您从 thunk 动作创建者返回的函数。这让 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鈥檛 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()仅用于有条件地分派不同的操作,请考虑将业务逻辑放入减速器中。

下一步

现在您已经对 thunk 的工作原理有了基本的了解,请查看使用它们的
Redux异步示例。

您可能会发现许多 thunk 返回 Promises 的示例。这不是必需的,但非常方便。Redux 并不关心你从 thunk
中返回什么,但它会为你提供从dispatch(). 这就是为什么你可以从一个 thunk 中返回一个 Promise
并通过调用dispatch(someThunkReturningPromise()).then(...).

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

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

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

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

除非您知道自己为什么要这样做,否则不要大汗淋漓。

2022-02-28