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

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


当前回答

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

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

引用官方文件:

什么是可还原可观察?

基于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;
}

我希望这能有所帮助!

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

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

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

我最终在<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} />;
}