如何分派一个超时的 Redux 动作?

How to dispatch a Redux action with a timeout?

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

我在使用 setTimeout 和 return 另一个动作时运气不佳,无法找到在线完成的方法。所以欢迎任何建议。

您可以使用 redux-thunk. There is a guide in redux document 执行此操作以执行 setTimeout 等异步操作。

不要陷入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)

唯一的区别是,在连接的组件中,您通常无法访问商店本身,但可以 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)

到目前为止,我们还没有使用任何中间件或其他高级概念。

正在提取 Async Action Creator

上述方法在简单情况下效果很好,但您可能会发现它有一些问题:

  • 它迫使你在任何你想显示通知的地方复制这个逻辑。
  • 通知没有 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 作为第一个参数?因为它需要向商店发送操作。通常一个组件可以访问 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.')    

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

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

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

回到以前的版本:

// 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() 并不是真正的动作创建者。它不是 return 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 作为第一个参数。相反,它 returns 一个接受 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 只会在 action 被调度后才会启动。如果您在 thunk 动作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它怎么办?

如果不使用 thunk 中间件,您只需在组件内部执行此检查:

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

但是,提取动作创建者的目的是将这种重复逻辑集中到许多组件中。幸运的是,Redux Thunk 为您提供了一种方法来读取 Redux 存储的当前状态。除了 dispatch 之外,它还会将 getState 作为第二个参数传递给来自你的 thunk 动作创建者的你 return 的函数。这让 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 return Promises 的示例。这不是必需的,但可能非常方便。 Redux 不关心你从 thunk 中 return 得到什么,但它从 dispatch() 中给你它的 return 值。这就是为什么你可以 return 来自 thunk 的 Promise 并通过调用 dispatch(someThunkReturningPromise()).then(...).

等待它完成

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

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

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

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

除非你知道你为什么这样做,否则不要着急。

我还建议您看看 SAM pattern

SAM 模式提倡包含一个 "next-action-predicate",一旦模型更新(SAM 模型 ~ reducer 状态 + 存储),就会触发 "notifications disappear automatically after 5 seconds" 等(自动)操作。

该模式提倡一次一个地对动作和模型突变进行排序,因为模型的 "control state" "controls" 启用哪些动作 and/or 由下一个动作自动执行谓词。您根本无法(通常)预测系统在处理一个动作之前将处于什么状态,因此您的下一个预期动作是否会是 allowed/possible。

例如代码,

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.

使用 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 个通知的情况下请求通知,则 queue/postpone 它。

结果

我的生产应用程序的屏幕截图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 个通知,第 1 个通知消失后,第 4 个会稍晚出现。

请注意,我不特别推荐从 JSX 调度 TOAST_DISPLAY_REQUESTED。您宁愿添加另一个 saga 来侦听您已经存在的应用程序事件,然后分派 TOAST_DISPLAY_REQUESTED:触发通知的组件不必与通知系统紧密耦合。

结论

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

甚至更容易实施更复杂的规则,例如:

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

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

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

在尝试了各种流行的方法(action creators、thunks、sagas、epics、effects、custom middleware)之后,我仍然觉得也许还有改进的余地,所以我在这篇博客文章中记录了我的旅程, Where do I put my business logic in a React/Redux application?

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

它允许您拦截验证、验证、授权的操作,并提供一种执行异步 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);
  }
});
    

我的存储库中有一个更高级的通知示例,其工作方式类似于 Sebastian Lorber 所描述的,您可以将显示限制为 N 个项目并轮流显示任何排队的项目。 redux-logic notification example

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

我很想听听您的反馈。

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

假设您的动作创作者看起来像这样:

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

timeout在上面的action中可以持有多个值

  • 以毫秒为单位的数字 - 对于特定的超时持续时间
  • 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

A repository with sample projects

目前有四个示例项目:

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

接受的答案很棒。

但是少了点什么:

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

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

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

Redux 传奇

接受的答案已经提供了 Async Code Inline、Async Action Generator 和 Redux Thunk 的示例代码片段。为了完整起见,我提供了 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

传奇基于 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 Generators。

如果上面的代码片段没有回答您的所有问题,请参阅 runnable project

正确的方法是使用 Redux Thunk 这是一个 Redux 的流行中间件,根据 Redux Thunk 文档:

"Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters".

所以基本上它 returns 是一个函数,您可以延迟发送或将其置于条件状态。

所以像这样的东西将为你完成这项工作:

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-observable 的解决方案。史诗

引用官方文档:

什么是 redux-observable?

RxJS 5-based middleware for Redux. Compose and cancel async actions to create side effects and more.

史诗是redux-observable的核心原语。

It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.

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

让我post代码然后再解释一下

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 一样,您必须组合 epics 以获得接收动作流的顶级函数和 returns 动作流,因此您可以将它与中间件工厂 createEpicMiddleware 一起使用。在我们的例子中,我们只需要一个,所以我们只有 rootEpic,所以我们不必组合任何东西,但这是一个很好的事实。

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

要点3.逐行rootEpic解释(在评论中)

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 这样的东西,它会提供一个 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],
});

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

为什么要这么难?这只是 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;
}

在这种情况下,问题应该是 "how do you clean up old state?"、"how to notify a component that time has changed"

您可以实施一些 TIMEOUT 操作,这些操作是在组件的 setTimeout 上分派的。

也许每当显示新通知时清除它就可以了。

总之,某处应该有一些setTimeout吧?为什么不在一个组件中做呢

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

动机是 "notification fade out" 功能确实是一个 UI 问题。因此它简化了对业务逻辑的测试。

测试它的实现方式似乎没有意义。只有验证通知何时超时才有意义。因此存根代码更少,测试更快,代码更清晰。

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

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

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

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

我最终在 <Alert /> 组件中使用了 setTimeout(),这样它就可以在给定的 id.[=15= 上调用和调度 REMOVE 操作]

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