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

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


你可以用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入门指南。

使用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之间的品味问题。

在尝试了各种流行的方法(动作创造者,坦克,传奇,史诗,效果,自定义中间件)后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,在React/Redux应用程序中我应该把我的业务逻辑放在哪里?

就像这里的讨论一样,我尝试对比和比较各种方法。最终,我引入了一个新的redux-logic库,它的灵感来自史诗、传奇故事和自定义中间件。

它允许您拦截验证、验证、授权的操作,并提供了一种执行异步IO的方法。

一些常见的功能可以简单地声明,如debashing、节流、取消,并且只使用来自最新请求的响应(takeLatest)。Redux-logic包装为您提供此功能的代码。

这使您可以随心所欲地实现核心业务逻辑。除非你愿意,否则你不必使用可观察对象或生成器。使用函数和回调,承诺,异步函数(async/await)等。

做一个简单的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); } });

在我的repo中,我有一个更高级的通知示例,其工作原理类似于Sebastian Lorber所描述的,您可以将显示限制为N个项目,并通过任何排队的项目进行旋转。Redux-logic通知示例

我有各种redux-logic jsfiddle现场的例子,以及完整的例子。我还在继续写文档和例子。

我很想听听你的反馈。

如果希望对选择性操作进行超时处理,可以尝试中间件方法。 我遇到过一个类似的问题,有选择地处理基于承诺的行为,这个解决方案更灵活。

假设你的动作创造者是这样的:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

Timeout可以在上述操作中包含多个值

以毫秒为单位的数字-用于指定的超时时间 True -用于固定的超时时间。(在中间件中处理) 未定义—用于立即分派

你的中间件实现看起来是这样的:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

现在,您可以使用redux将所有操作路由到这个中间件层。

createStore(reducer, applyMiddleware(timeoutMiddleware))

你可以在这里找到一些类似的例子

带有示例项目的存储库

目前有四个样本项目:

内联编写异步代码 提取异步操作创建器 使用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坦克,这是一个 Redux的流行中间件,根据Redux坦克文档:

Redux坦克中间件允许你编写动作创建者 返回一个函数而不是一个动作。坦克可以用来拖延时间 一个动作的调度,或者只有在某种条件下才进行调度 是满足。内部函数接收存储方法的调度和 getState作为参数”。

基本上它会返回一个函数,你可以延迟分派或者把它置于条件状态。

所以像这样的东西会帮你完成工作:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}

我知道这个问题有点老了,但我将介绍另一种解决方案,使用还原可观测。史诗。

引用官方文件:

什么是可还原可观察?

基于RxJS 5的Redux中间件。合成和取消异步操作 创造副作用等等。

史诗是还原可观察的核心原语。

它是一个接受操作流并返回流的函数 的行动。行动进,行动出。

简而言之,您可以创建一个通过流接收操作的函数,然后返回一个新的操作流(使用常见的副作用,如超时、延迟、间隔和请求)。

让我发布代码,然后再详细解释一下

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

解决这个问题的关键代码就像你看到的一样简单,唯一与其他答案不同的是rootpic函数。

点1。与saga一样,为了获得一个顶级函数来接收动作流并返回动作流,您必须将这些epics组合在一起,因此可以将它与中间件工厂createEpicMiddleware一起使用。在我们的例子中,我们只需要一个,所以我们只有rootEpic,所以我们不需要组合任何东西,但知道事实是很好的。

点2。我们的rootEpic只需要5行代码就可以处理副作用,这太棒了!包括这几乎是声明性的事实!

点3。逐行解析(在注释中)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

我希望这能有所帮助!

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

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

为什么这么难呢?这只是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关注点。因此,它简化了业务逻辑的测试。

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

这很简单。使用trim-redux包,在componentDidMount或其他地方这样写,并在componentWillUnmount中杀死它。

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

这可能有点离题,但我想在这里分享它,因为我只是想在给定超时后从状态中删除警报,即自动隐藏警报/通知。

我最终在<Alert />组件中使用setTimeout(),以便它可以在给定id上调用和分派REMOVE操作。

export function Alert(props: Props) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      dispatchAction({
        type: REMOVE,
        payload: {
          id: id,
        },
      });
    }, timeout ?? 2000);
    return () => clearTimeout(timeoutID);
  }, []);
  return <AlertComponent {...props} />;
}

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

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

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