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

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


当前回答

我建议大家也看看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入门指南。

其他回答

使用Redux-saga

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

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

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

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

高级通知系统规范

您可以请求显示通知 您可以请求隐藏通知 通知的显示时间不应超过4秒 可以同时显示多个通知 同时显示的通知不能超过3条 如果一个通知被请求而已经有3个显示的通知,那么排队/延迟它。

结果

我的制作应用Stample.co的截图

Code

这里我将通知命名为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)
    ]
}

以及减速机:

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)

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

注意,你可以用redux-observable做同样的事情,它与redux-saga非常相似。它几乎是一样的,只是生成器和RxJS之间的品味问题。

我建议大家也看看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入门指南。

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

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

带有示例项目的存储库

目前有四个样本项目:

内联编写异步代码 提取异步操作创建器 使用Redux坦克 使用Redux Saga

公认的答案是棒极了。

但这里缺少了一些东西:

没有可运行的示例项目,只有一些代码片段。 没有其他替代方案的示例代码,例如: 回家的故事

所以我创建了Hello Async存储库来添加缺少的东西:

可运行的项目。您可以下载并运行它们而无需修改。 提供更多替代方案的示例代码: 回家的故事 回来的循环 ...

回家的故事

接受的答案已经提供了异步代码内联,异步动作生成器和Redux坦克的示例代码片段。为了完整起见,我提供了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 saga
let nextNotificationId = 0
function* 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

saga基于ES6 Generators

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

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

sagaMiddleware.run(saga)

与Redux坦克相比

Pros

你不会去回试的地狱。 您可以轻松地测试异步流。 你的行为保持纯净。

Cons

它依赖于相对较新的ES6 Generators。

如果上面的代码片段不能回答您的所有问题,请参考可运行项目。

你可以用redux-thunk做到这一点。redux文档中有关于setTimeout等异步操作的指南。