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

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


当前回答

Redux本身是一个非常冗长的库,对于这样的东西,你必须使用像Redux-thunk这样的东西,它会提供一个分派函数,所以你将能够在几秒钟后分派关闭通知。

我已经创建了一个库来解决诸如冗长性和可组合性等问题,您的示例将如下所示:

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 notifications
  nesting: ({ 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],
});

因此,我们在异步动作中组合同步动作来显示通知,这可以请求一些后台信息,或者稍后检查通知是否被手动关闭。

其他回答

Redux操作只能返回一个普通对象,而不是函数、回调或异步进程。为了通过web API(如timeout()方法)分派它们,你必须使用redux-thunk中间件。创建它是为了处理这样的流程。

首先通过文档配置redux-thunk 第二,这样改变你的动作创建器:

const yourAction = millisecond => dispatch => {
   setTimeout(() => {
      dispatch({
         type: 'YOUR_ACTIION_TYPE',
         payload: yourWhatEverPayload
      })
   }, millisecond)
}

为什么这么难呢?这只是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;
}

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

您可以实现一些TIMEOUT动作,该动作在组件的setTimeout上分派。

也许在显示新通知时清理它就可以了。

总之,应该有一些setTimeout,对吧?为什么不在组件中实现呢

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

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

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

你可以用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.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’d 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.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.')    

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

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

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

回到之前的版本:

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

这解决了逻辑重复的问题,并将我们从竞争条件中拯救出来。

铛中间件

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

然而,在较大的应用程序中,你可能会发现一些不方便。

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

此外,很难记住哪些函数是同步操作创建者(如showNotification()),哪些是异步帮助器(如showNotificationWithTimeout())。你必须以不同的方式使用它们,小心不要把它们弄错。

这就是寻找一种方法来“合法化”这种向helper函数提供分派的模式的动机,并帮助Redux“将”这种异步操作创建者视为普通操作创建者的特殊情况,而不是完全不同的函数。

如果你仍然和我们在一起,你也意识到在你的应用程序中的一个问题,欢迎你使用Redux坦克中间件。

总的来说,Redux坦克教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)
})

当这个中间件被启用时,如果你分派一个函数,Redux坦克中间件会把它作为一个参数分派。它也会“吞下”这样的动作,所以不用担心你的约简会收到奇怪的函数参数。您的约简器将只接收普通对象操作——要么直接发出,要么由我们刚才描述的函数发出。

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

我们调用async action创建者来获得内部函数,它只需要分派,然后传递分派。

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

因为我之前告诉过你。如果Redux坦克中间件是启用的,任何时候你试图分派一个函数而不是一个动作对象,中间件将调用该函数并将分派方法本身作为第一个参数。

所以我们可以这样做:

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

在坦克中阅读国家

通常,约简器包含用于确定下一个状态的业务逻辑。然而,减量只在行动被分派之后才会起作用。如果你在坦克动作创建器中有副作用(比如调用API),而你想在某些情况下阻止它怎么办?

如果不使用thunk中间件,你只需要在组件内部进行检查:

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

然而,提取动作创建器的目的是将这种重复的逻辑集中到许多组件上。幸运的是,Redux坦克为您提供了一种读取Redux存储的当前状态的方法。除了分派,它还将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’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()来有条件地分派不同的操作,那么可以考虑将业务逻辑放到简化器中。

下一个步骤

现在你已经有了关于坦克如何工作的基本直觉,看看Redux async例子,它使用了它们。

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

你也可以把复杂的坦克动作创造者分成几个更小的坦克动作创造者。由thunks提供的分派方法可以接受thunk本身,因此您可以递归地应用该模式。同样,这对于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. 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.

最后,不要使用任何你没有真正需要的东西(包括坦克)。请记住,根据需求的不同,您的解决方案可能看起来非常简单

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

别担心,除非你知道你为什么这么做。

我建议大家也看看SAM模式。

SAM模式提倡包含“下一个动作-谓词”,其中一旦模型更新(SAM模型~ reducer状态+ store),就会触发(自动)动作,例如“通知在5秒后自动消失”。

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

比如代码,

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

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

在SAM中是不允许的,因为hideNotification动作可以被分派的事实依赖于模型成功接受值" shownotice: true"。模型的其他部分可能阻止它接受,因此,没有理由触发hideNotification操作。

我强烈建议在存储更新和模型的新控件状态可以知道之后实现适当的下一个操作谓词。这是实现您正在寻找的行为的最安全的方法。

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