我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 5 秒后调度另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在 5 秒后自动消失的功能。
我在使用setTimeout
和返回另一个操作方面没有运气,也找不到在线完成此操作的方式。因此,欢迎任何建议。
我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 5 秒后调度另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在 5 秒后自动消失的功能。
我在使用setTimeout
和返回另一个操作方面没有运气,也找不到在线完成此操作的方式。因此,欢迎任何建议。
不要陷入认为图书馆应该规定如何做所有事情的陷阱。如果你想在 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()
特定的动作创建者作为props注入。然而,这对我们来说没有任何区别。
如果您不喜欢在从不同组件分派相同操作时犯错,您可能希望提取操作创建者而不是内联分派操作对象:
// 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
但由于我们想要一个外部函数来控制分派,我们需要让它控制分派。
如果你有一个从某个module导出的单例存储,你可以直接导入它并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 成为单例。这使得实现服务器渲染变得非常困难。在服务器上,您会希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。
单例存储也使测试更加困难。在测试动作创建者时,您不能再模拟商店,因为它们引用从特定module导出的特定真实商店。您甚至无法从外部重置其状态。
因此,虽然您在技术上可以从module导出单例存储,但我们不鼓励这样做。除非您确定您的应用程序永远不会添加服务器渲染,否则不要这样做。
回到之前的版本:
// 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.')
这解决了逻辑重复的问题,并使我们免于竞争条件。
对于简单的应用程序,该方法应该足够了。如果您对中间件感到满意,请不要担心它。
但是,在较大的应用程序中,您可能会发现它存在某些不便之处。
例如,我们不得不dispatch
绕过似乎很不幸。这使得分离容器和展示组件变得更加棘手,因为任何以上述方式异步调度 Redux 操作的组件都必须接受dispatch
作为一个 prop 才能进一步传递它。你不能再绑定动作创建者,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 只会接收普通的对象动作——直接发出,或者由我们刚刚描述的函数发出。
这看起来不是很有用,是吗?不是在这种特殊情况下。然而,它让我们声明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)
我们正在调用 async action creator 来获取想要的内部函数,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)
通常,您的减速器包含用于确定下一个状态的业务逻辑。然而,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 示例。
你可能会发现很多 thunk 返回 Promise 的例子。这不是必需的,但可能非常方便。Redux 不关心你从 thunk 返回什么,但它从dispatch()
. 这就是为什么您可以从 thunk 返回一个 Promise 并通过调用dispatch(someThunkReturningPromise()).then(...)
.
您还可以将复杂的 thunk 动作创建器拆分为几个较小的 thunk 动作创建器。dispatch
thunk 提供的方法可以接受 thunk 本身,因此您可以递归地应用该模式。同样,这对 Promise 最有效,因为您可以在其之上实现异步控制流。
对于某些应用程序,您可能会发现自己的异步控制流要求过于复杂,无法用 thunk 来表达。例如,重试失败的请求、使用令牌重新授权流程或分步引导在以这种方式编写时可能过于冗长且容易出错。在这种情况下,您可能需要查看更高级的异步控制流解决方案,例如Redux Saga或Redux Loop。评估它们,比较与您的需求相关的示例,然后选择您最喜欢的示例。
最后,如果您真的不需要它们,请不要使用任何东西(包括 thunk)。请记住,根据要求,您的解决方案可能看起来很简单
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 中发现无限 while 循环的原因)。不知何故,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;
}
};
您可以简单地调度TOAST_DISPLAY_REQUESTED
事件。如果你发送 4 个请求,只会显示 3 个通知,第 4 个会在第一个通知消失后出现。
请注意,我不特别推荐TOAST_DISPLAY_REQUESTED
从 JSX调度。您宁愿添加另一个 saga 来侦听您已经存在的应用程序事件,然后分派TOAST_DISPLAY_REQUESTED
: 触发通知的组件,不必与通知系统紧密耦合。
我的代码并不完美,但在生产环境中运行了几个月,并且有 0 个错误。Redux-saga 和生成器最初有点难,但是一旦你理解了它们,这种系统就很容易构建。
实现更复杂的规则甚至很容易,例如:
老实说,祝你好运用 thunk 正确地实现这种东西。
请注意,您可以使用redux-observable做完全相同的事情,它与 redux-saga 非常相似。这几乎是一样的,是生成器和 RxJS 之间的品味问题。
目前有四个示例项目:
接受的答案很棒。
但是还缺少一些东西:
所以我创建了Hello Async存储库来添加缺少的东西:
已接受的答案已经为 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
Sagas 基于ES6 生成器
// 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做到这一点。redux 文档中有一个关于异步操作的指南,比如 setTimeout。
我还建议您查看SAM 模式。
SAM 模式提倡包括一个“下一个动作谓词”,其中(自动)动作,例如“通知在 5 秒后自动消失”,一旦模型更新(SAM 模型~reducer 状态 + 存储)就会触发。
该模式提倡一次一个地对动作和模型突变进行排序,因为模型的“控制状态”“控制”了下一个动作谓词启用和/或自动执行哪些动作。您根本无法预测(通常)系统在处理操作之前将处于什么状态,因此您的下一个预期操作是否会被允许/可能。
所以例如代码,
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
SAM 不允许,因为可以调度 hideNotification 操作的事实取决于模型成功接受值“showNotification: true”。模型的其他部分可能会阻止它接受它,因此没有理由触发 hideNotification 操作。
我强烈建议在商店更新并且可以知道模型的新控制状态之后实施适当的下一步操作谓词。这是实现您正在寻找的行为的最安全的方法。
如果你愿意,你可以在 Gitter 上加入我们。这里还有一份SAM 入门指南。