如何调度具有超时的 Redux 操作?
我有一个操作,用于更新应用程序的通知状态。通常,此通知将是某种错误或信息。然后,我需要在5秒后调度另一个操作,该操作会将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。
我没有运气使用和返回另一个操作,并且无法在线找到如何完成此操作。因此,欢迎任何建议。setTimeout
我有一个操作,用于更新应用程序的通知状态。通常,此通知将是某种错误或信息。然后,我需要在5秒后调度另一个操作,该操作会将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。
我没有运气使用和返回另一个操作,并且无法在线找到如何完成此操作。因此,欢迎任何建议。setTimeout
不要陷入这样的陷阱,认为图书馆应该规定如何做所有事情。如果你想在JavaScript中做一些超时的事情,你需要使用。Redux 操作没有理由有任何不同。setTimeout
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)
到目前为止,我们还没有使用任何中间件或其他高级概念。
上述方法在简单情况下工作正常,但您可能会发现它存在一些问题:
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.')
这看起来更简单,但我们不建议使用此方法。我们不喜欢它的主要原因是因为它迫使商店成为单例。这使得实现服务器渲染非常困难。在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。
单例商店也使测试更加困难。在测试操作创建者时,您无法再模拟存储,因为它们引用了从特定模块导出的特定实际存储。您甚至无法从外部重置其状态。
因此,虽然从技术上讲,您可以从模块导出单例存储,但我们不鼓励这样做。除非你确定你的应用永远不会添加服务器呈现,否则不要执行此操作。
回到以前的版本:
// 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 操作的组件都必须接受为 prop,以便它可以进一步传递它。您不能再将操作创建者绑定在一起,因为它不是真正的操作创建者。它不会返回 Redux 操作。dispatch
dispatch
connect()
showNotificationWithTimeout()
此外,记住哪些函数是同步操作创建者喜欢的,哪些是异步帮助器(如 )。您必须以不同的方式使用它们,并注意不要将它们相互误解。showNotification()
showNotificationWithTimeout()
这是找到一种方法的动机,使这种向帮助程序函数提供调度
的模式“合法化”,并帮助 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
这看起来不是很有用,不是吗?不是在这种特殊情况下。但是,它允许我们声明为常规的 Redux 操作创建者:showNotificationWithTimeout()
// 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 中间件,则每当您尝试调度函数而不是操作对象时,中间件都会以调度
方法本身作为第一个参数来调用该函数。
因此,我们可以这样做:
// 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)
通常,化简器包含用于确定下一个状态的业务逻辑。但是,化简器仅在调度操作后才启动。如果您在 thunk 操作创建器中存在副作用(例如调用 API),并且想要在某种条件下防止它,该怎么办?
如果不使用 thunk 中间件,您只需在组件内部执行以下检查:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
但是,提取操作创建者的要点是将此重复逻辑集中在许多组件中。幸运的是,Redux Thunk 为您提供了一种读取 Redux 商店当前状态的方法。除了 之外,它还作为第二个参数传递给您从 thunk 操作创建器返回的函数。这允许 thunk 读取存储的当前状态。dispatch
getState
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()
现在您已经对 thunks 的工作原理有了基本的直觉,请查看使用它们的 Redux 异步示例。
你可能会发现很多例子,其中thunks返回承诺。这不是必需的,但可以非常方便。Redux 并不关心你从 thunk 返回什么,但它从 中给你返回值。这就是为什么您可以从thunk返回承诺并通过调用来等待它完成的原因。dispatch()
dispatch(someThunkReturningPromise()).then(...)
您还可以将复杂的 thunk 动作创建者拆分为几个较小的 thunk 动作创作者。thunks 提供的方法可以接受 thunks 本身,因此您可以递归地应用该模式。同样,这最适合 Promises,因为您可以在此之上实现异步控制流。dispatch
对于某些应用,你可能会发现自己处于异步控制流要求过于复杂而无法用 thunk 表示的情况。例如,以这种方式编写时,重试失败的请求、使用令牌的重新授权流或分步载入可能过于冗长且容易出错。在这种情况下,您可能需要查看更高级的异步控制流解决方案,例如 Redux Saga 或 Redux Loop。评估它们,比较与您的需求相关的示例,然后选择您最喜欢的示例。
最后,如果你没有真正需要的东西(包括thunks),不要使用它们。请记住,根据要求,您的解决方案可能看起来很简单
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
不要出汗,除非你知道你为什么要这样做。
正如 Dan Abramov 所说,如果你想对你的异步代码进行更高级的控制,你可以看看 redux-saga。
这个答案是一个简单的例子,如果你想更好地解释为什么redux-saga对你的应用程序有用,请查看这个其他答案。
一般的想法是,Redux-saga提供了一个ES6生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么您经常会在Redux-saga中找到无限的循环)。不知何故,Redux-saga直接在Javascript中构建了自己的语言。Redux-saga起初可能会感觉有点难以学习,因为您需要对生成器有基本的了解,但也需要了解Redux-saga提供的语言。
我将尝试在这里描述我在 redux-saga 之上构建的通知系统。此示例当前在生产环境中运行。
我的生产应用 Stample.co 的屏幕截图
在这里,我将通知命名为a,但这是一个命名细节。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;
}
};
您只需分派事件即可。如果您调度 4 个请求,则只会显示 3 个通知,第 4 个通知将在第 1 个通知消失后稍晚显示。TOAST_DISPLAY_REQUESTED
请注意,我并不特别建议从 JSX 进行调度。你更愿意添加另一个侦听现有应用事件的 saga,然后调度 : 触发通知的组件不必与通知系统紧密耦合。TOAST_DISPLAY_REQUESTED
TOAST_DISPLAY_REQUESTED
我的代码并不完美,但在生产环境中运行了几个月,bug为0。Redux-saga和生成器最初有点困难,但是一旦你了解了它们,这种系统就很容易构建。
实现更复杂的规则甚至很容易,例如:
老实说,祝你好运,用thunks正确地实现这种东西。
请注意,您可以使用redux-observable做完全相同的事情,这与redux-saga非常相似。这几乎是一样的,是生成器和RxJS之间的品味问题。