【问题标题】:How to dispatch a Redux action with a timeout?如何在超时的情况下调度 Redux 操作?
【发布时间】:2016-05-26 11:37:45
【问题描述】:

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

我没有运气使用setTimeout 并返回另一个操作,并且无法在网上找到这是如何完成的。所以欢迎任何建议。

【问题讨论】:

  • 如果你想要比 thunk 更好的东西,别忘了检查我基于redux-saga 的答案。迟到的答案,所以你必须滚动很长时间才能看到它出现:) 并不意味着它不值得阅读。这是一个快捷方式:stackoverflow.com/a/38574266/82609
  • 每当你执行 setTimeout 时,不要忘记在 componentWillUnMount 生命周期方法中使用 clearTimeout 清除计时器
  • redux-saga 很酷,但它们似乎不支持来自生成器函数的类型化响应。如果您使用带有反应的打字稿可能很重要。

标签: javascript redux react-redux timeout


【解决方案1】:

使用 Redux-saga

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

这个答案是一个简单的例子,如果您想更好地解释为什么 redux-saga 对您的应用程序有用,请查看 this other answer

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

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

高级通知系统规范

  • 您可以请求显示通知
  • 您可以请求隐藏通知
  • 通知的显示时间不应超过 4 秒
  • 可以同时显示多个通知
  • 不能同时显示超过 3 个通知
  • 如果在已显示 3 个通知时请求通知,则将其排队/推迟。

结果

我的生产应用程序的屏幕截图Stample.co

代码

这里我将通知命名为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:触发通知的组件不必与通知系统紧密耦合。

结论

我的代码并不完美,但在生产环境中运行了几个月,出现了 0 个错误。 Redux-saga 和生成器一开始有点难,但是一旦你理解了它们,这种系统就很容易构建。

实现更复杂的规则甚至非常容易,例如:

  • 当“排队”的通知过多时,减少每个通知的显示时间,以便队列大小可以更快地减小。
  • 检测窗口大小变化,并相应更改显示通知的最大数量(例如,桌面 = 3,手机纵向 = 2,手机横向 = 1)

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

请注意,您可以使用 redux-observable 做完全相同的事情,这与 redux-saga 非常相似。这几乎是一样的,只是生成器和 RxJS 之间的口味问题。

【讨论】:

  • 我希望你的答案在被问到的时候早点出现,因为我非常同意将 Saga 副作用库用于这样的业务逻辑。 Reducers 和 Action Creators 用于状态转换。工作流与状态转换函数不同。工作流逐步通过转换,但本身不是转换。 Redux + React 自己缺乏这个——这正是 Redux Saga 如此有用的原因。
  • 谢谢,由于这些原因,我尽力让 redux-saga 流行起来 :) 很少有人认为目前 redux-saga 只是 thunk 的替代品,并且看不到 redux-saga支持复杂且解耦的工作流程
  • 没错。 Actions & Reducers 都是状态机的一部分。有时,对于复杂的工作流程,您需要其他东西来编排不直接属于状态机本身的状态机!
  • 操作:有效负载/事件到转换状态。 Reducers:状态转换函数。组件:反映状态的用户界面。但是缺少一个主要部分 - 您如何管理许多转换的过程,这些转换都有自己的逻辑来确定接下来要执行的转换? Redux 传奇!
  • @mrbrdo 如果您仔细阅读我的回答,您会注意到通知超时实际上是使用yield call(delay,timeoutValue); 处理的:它不是同一个 API,但它具有相同的效果
【解决方案2】:

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

我最终在&lt;Alert /&gt; 组件中使用了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} />;
}

【讨论】:

    【解决方案3】:

    您可以使用redux-thunk 执行此操作。有一个 guide in redux document 用于 setTimeout 等异步操作。

    【讨论】:

    • 只是一个快速的后续问题,当使用中间件applyMiddleware(ReduxPromise, thunk)(createStore) 时,您是这样添加几个中间件的(逗号分隔的?),因为我似乎无法让 thunk 工作。
    • @Ilja 这应该可以:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
    【解决方案4】:

    不要落入trap of thinking a library should prescribe how to do everything。如果你想在 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)
    

    唯一的区别是,在连接的组件中,您通常无法访问 store 本身,但可以将 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 发送操作。通常一个组件可以访问dispatch,但是由于我们想要一个外部函数来控制调度,我们需要让它控制调度。

    如果你有一个从某个模块导出的单例存储,你可以直接导入它并直接在其上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.')    
    

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

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

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

    回到以前的版本:

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

    这解决了逻辑重复的问题,并使我们免于竞争条件。

    Thunk 中间件

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

    但是,在较大的应用程序中,您可能会发现一些不便之处。

    例如,我们不得不传递dispatch 似乎很不幸。这使得separate container and presentational components 变得更加棘手,因为任何以上述方式异步调度 Redux 操作的组件都必须接受 dispatch 作为道具,以便它可以进一步传递它。您不能再将动作创建者与connect() 绑定,因为showNotificationWithTimeout() 并不是真正的动作创建者。它不返回 Redux 操作。

    此外,记住哪些函数是像showNotification() 这样的同步动作创建者以及哪些是像showNotificationWithTimeout() 这样的异步助手可能会很尴尬。您必须以不同的方式使用它们,并注意不要将它们弄错。

    这就是找到一种方法来“合法化”这种将dispatch 提供给辅助函数的模式的动机,并帮助 Redux 将这种异步动作创建者“视为”普通动作创建者的特例 而不是完全不同的功能。

    如果您仍然与我们在一起,并且您还发现您的应用存在问题,欢迎您使用Redux Thunk 中间件。

    概括地说,Redux Thunk 教 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 Thunk 中间件会给它dispatch 作为参数。它也会“吞下”这样的动作,所以不用担心你的 reducer 会收到奇怪的函数参数。你的 reducer 只会接收普通的对象动作——要么直接发出,要么由我们刚刚描述的函数发出。

    这看起来不是很有用,是吗?不是在这种特殊情况下。但是它让我们可以将 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)
    

    我们正在调用异步操作创建者来获取只需要dispatch 的内部函数,然后我们传递dispatch

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

    因为我之前告诉过你。 如果启用了 Redux Thunk 中间件,则任何时候您尝试调度函数而不是操作对象时,中间件都会以 dispatch 方法本身作为第一个参数来调用该函数

    所以我们可以这样做:

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

    Thunks 中的读取状态

    通常,您的 reducer 包含用于确定下一个状态的业务逻辑。但是,reducer 仅在动作被调度后才开始。如果您在 thunk 操作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它怎么办?

    不使用 thunk 中间件,您只需在组件内部进行以下检查:

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

    但是,提取动作创建者的目的是将这种重复逻辑集中到许多组件中。幸运的是,Redux Thunk 为您提供了一种读取 Redux 存储当前状态的方法。除了dispatch,它还将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() 有条件地分派不同的操作,请考虑将业务逻辑放入reducer 中。

    后续步骤

    既然您对 thunk 的工作原理有了基本的了解,请查看使用它们的 Redux async example

    您可能会发现许多 thunk 返回 Promise 的示例。这不是必需的,但非常方便。 Redux 不关心你从 thunk 中返回什么,但它会为你提供来自 dispatch() 的返回值。这就是为什么您可以通过调用 dispatch(someThunkReturningPromise()).then(...) 从 thunk 返回 Promise 并等待它完成的原因。

    您还可以将复杂的 thunk 动作创建者拆分为几个较小的 thunk 动作创建者。 thunks 提供的dispatch 方法本身可以接受thunks,因此您可以递归地应用该模式。同样,这最适合 Promises,因为您可以在此基础上实现异步控制流。

    对于某些应用,您可能会发现自己的异步控制流要求过于复杂而无法用 thunk 来表达。例如,以这种方式编写时,重试失败的请求、使用令牌的重新授权流程或分步入职可能过于冗长且容易出错。在这种情况下,您可能希望查看更高级的异步控制流解决方案,例如 Redux SagaRedux Loop。评估它们,比较与您的需求相关的示例,然后选择您最喜欢的示例。

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

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

    除非您知道自己为什么要这样做,否则不要担心。

    【讨论】:

    • 异步操作似乎是一个常见问题的简单而优雅的解决方案。为什么在不需要中间件的情况下对它们的支持不支持 redux?这个答案可能会更加简洁。
    • @PhilMander 因为有许多替代模式,如github.com/raisemarketplace/redux-loopgithub.com/yelouafi/redux-saga,它们同样(如果不是更多)优雅。 Redux 是一个低级工具。您可以构建自己喜欢的超集并单独分发。
    • 你能解释一下吗:*考虑将业务逻辑放入reducer *,这是否意味着我应该派发一个动作,然后根据我的状态在reducer中确定要派发哪些进一步的动作?我的问题是,我是否直接在我的 reducer 中调度其他操作,如果不是,那么我从哪里调度它们?
    • 这句话只适用于同步情况。例如,如果你写if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' }),也许你应该只写dispatch({ type: 'C', something: cond }),然后根据action.something 和当前状态选择忽略reducers 中的操作。
    • @DanAbramov 你得到了我的支持,“除非你有这个问题,否则请使用该语言提供的内容并选择最简单的解决方案。”才知道是谁写的!
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      我知道这个问题有点老了,但我将介绍另一个使用 redux-observable 的解决方案。史诗。

      引用官方文档:

      什么是 redux-observable?

      用于 Redux 的基于 RxJS 5 的中间件。编写和取消异步操作以 产生副作用等等。

      Epic 是 redux-observable 的核心原语。

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

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

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

      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)
      

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

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

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

      第3点。逐行rootEpic解释(以cmets为单位)

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

      希望对你有帮助!

      【讨论】:

      • 能否解释一下这里具体的api方法是做什么的,比如switchMap
      • 我们在 Windows 上的 React Native 应用程序中使用 redux-observable。这是一个针对复杂、高度异步问题的优雅实现解决方案,并通过他们的 Gitter 频道和 GitHub 问题获得了极好的支持。当然,只有当你找到它要解决的确切问题时,额外的复杂层才是值得的。
      【解决方案7】:

      执行此操作的适当方法是使用 Redux Thunk,它是 根据 Redux Thunk 文档,用于 Redux 的流行中间件:

      “Redux Thunk 中间件允许您编写动作创建器, 返回一个函数而不是一个动作。 thunk 可用于延迟 动作的派发,或仅在特定条件下派发 满足。内部函数接收 store 方法调度和 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);
        };
      }
      

      【讨论】:

        【解决方案8】:

        为什么要这么难?这只是 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 问题。因此,它可以简化您的业务逻辑测试。

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

        【讨论】:

        • 这应该是最佳答案。
        【解决方案9】:

        Redux 本身是一个非常冗长的库,对于这样的东西,你必须使用类似Redux-thunk 的东西,它会提供一个dispatch 函数,这样你就可以在几秒钟后调度关闭通知。

        I have created a library 解决冗长和可组合性等问题,您的示例将如下所示:

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

        所以我们编写同步操作来在异步操作中显示通知,它可以在后台请求一些信息,或者稍后检查通知是否被手动关闭。

        【讨论】:

          【解决方案10】:

          A repository with sample projects

          目前有四个示例项目:

          1. Writing Async Code Inline
          2. Extracting Async Action Creator
          3. Use Redux Thunk
          4. Use Redux Saga

          接受的答案很棒。

          但还是少了点什么:

          1. 没有可运行的示例项目,只有一些代码 sn-ps。
          2. 没有其他替代方案的示例代码,例如:
            1. Redux Saga

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

          1. 可运行的项目。您无需修改​​即可下载并运行它们。
          2. 提供更多替代方案的示例代码:

          Redux 传奇

          已接受的答案已经为 Async Code Inline、Async Action Generator 和 Redux Thunk 提供了示例代码 sn-ps。为了完整起见,我为 Redux Saga 提供了代码 sn-ps:

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

          Sagas 基于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 Thunk 相比

          优点

          • 您不会陷入回调地狱。
          • 您可以轻松测试异步流程。
          • 您的行为保持纯洁。

          缺点

          • 它依赖于相对较新的 ES6 生成器。

          如果上面的代码 sn-ps 不能回答您的所有问题,请参考runnable project

          【讨论】:

            【解决方案11】:

            如果您想对选择性操作进行超时处理,您可以尝试middleware 方法。 我在选择性地处理基于承诺的操作时遇到了类似的问题,这种解决方案更加灵活。

            假设您的动作创建者如下所示:

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

            你可以找到一些类似的例子here

            【讨论】:

              【解决方案12】:

              在尝试了各种流行的方法(动作创建器、thunk、sagas、史诗、效果、自定义中间件)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,Where do I put my business logic in a React/Redux application?

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

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

              可以简单地声明一些常见功能,例如去抖动、限制、取消,并且仅使用来自最新请求的响应 (takeLatest)。 redux-logic 包装了为您提供此功能的代码。

              这让您可以随意实现核心业务逻辑。除非您愿意,否则您不必使用可观察对象或生成器。使用函数和回调、promise、异步函数(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 notification example

              我有各种redux-logic jsfiddle live examples as well as full examples。我将继续处理文档和示例。

              我很想听听您的反馈。

              【讨论】:

              • 我不确定我是否喜欢您的图书馆,但我确实喜欢您的文章!干得好,伙计!你已经做了足够多的工作来节省别人的时间。
              • 我在这里为 redux-logic 创建了一个示例项目:github.com/tylerlong/hello-async/tree/master/redux-logic 我认为这是一个设计良好的软件,与其他替代方案相比,我没有看到任何主要缺点。
              【解决方案13】:

              我还建议您查看SAM pattern

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

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

              例如代码,

              export function showNotificationWithTimeout(dispatch, text) {
                const id = nextNotificationId++
                dispatch(showNotification(id, text))
              
                setTimeout(() => {
                  dispatch(hideNotification(id))
                }, 5000)
              }
              

              不允许使用 SAM,因为可以调度 hideNotification 操作这一事实取决于模型是否成功接受了值“showNotication: true”。模型的其他部分可能会阻止它接受它,因此没有理由触发 hideNotification 操作。

              我强烈建议在商店更新后实施适当的下一步操作谓词,并且可以知道模型的新控制状态。这是实现您正在寻找的行为的最安全方式。

              如果您愿意,可以在 Gitter 上加入我们。还有一个SAM getting started guide available here

              【讨论】:

              • 到目前为止,我只触及了表面,但 SAM 模式已经让我兴奋不已。 V = S( vm( M.present( A(data) ) ), nap(M)) 很漂亮。感谢您分享您的想法和经验。我会深入挖掘。
              • @ftor,谢谢!第一次写的时候,也有同样的感觉。我已经在生产环境中使用 SAM 将近一年了,我想不出有什么时候我觉得我需要一个库来实现 SAM(甚至是 vdom,尽管我可以看到它什么时候可以使用)。只需一行代码,就可以了! SAM 生成同构代码,如何处理异步调用没有歧义......我想不出我在什么时候,我在做什么?
              • SAM 是一种真正的软件工程模式(刚刚用它制作了一个 Alexa SDK)。它基于 TLA+,并试图将这项令人难以置信的工作的力量带给每个开发人员。 SAM 纠正了(几乎)每个人几十年来一直在使用的三个近似值: - 动作可以操纵应用程序状态 - 赋值等同于突变 - 没有精确定义编程步骤是什么(例如,a = b * ca 步骤, 1/ 读取 b,c 2/ 计算 b*c, 3/ 将结果分配给 a 三个不同的步骤?
              猜你喜欢
              • 2022-01-20
              • 1970-01-01
              • 2018-05-22
              • 1970-01-01
              • 1970-01-01
              • 2017-11-09
              • 2010-12-28
              • 2018-11-20
              • 2017-07-20
              相关资源
              最近更新 更多