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

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


当前回答

带有示例项目的存储库

目前有四个样本项目:

内联编写异步代码 提取异步操作创建器 使用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操作只能返回一个普通对象,而不是函数、回调或异步进程。为了通过web API(如timeout()方法)分派它们,你必须使用redux-thunk中间件。创建它是为了处理这样的流程。

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

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

我建议大家也看看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之间的品味问题。

正确的方法是使用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);
  };
}

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

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