我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 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)
唯一的区别是,在连接的组件中,您通常无法访问 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)
到目前为止,我们还没有使用任何中间件或其他高级概念。
上面的方法在简单的情况下可以正常工作,但您可能会发现它存在一些问题:
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 成为单例。这使得实现服务器渲染变得非常困难。在服务器上,您会希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。
单例商店也使测试变得更加困难。在测试动作创建者时,您不能再模拟商店,因为它们引用了从特定模块导出的特定真实商店。您甚至无法从外部重置其状态。
因此,虽然您在技术上可以从模块中导出单例存储,但我们不鼓励这样做。除非您确定您的应用永远不会添加服务器渲染,否则不要这样做。
回到以前的版本:
// 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
作为道具,以便它可以进一步传递它。你不能仅仅绑定动作创建者,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)
})
启用此中间件后,如果您 dispatch 一个函数,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)
通常你的 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()
仅用于有条件地分派不同的操作,请考虑将业务逻辑放入减速器中。
现在您已经对 thunk 的工作原理有了基本的了解,请查看使用它们的 Redux异步示例。
您可能会发现许多 thunk 返回 Promises 的示例。这不是必需的,但非常方便。Redux 不关心你从 thunk 返回什么,但它会为你提供它的返回值 from dispatch()
。这就是为什么你可以从一个 thunk 中返回一个 Promise 并通过调用dispatch(someThunkReturningPromise()).then(...)
.
您还可以将复杂的 thunk 动作创建者拆分为几个较小的 thunk 动作创建者。thunks 提供的dispatch
方法可以接受 thunk 本身,因此您可以递归地应用该模式。同样,这对 Promises 最有效,因为您可以在此基础上实现异步控制流。
对于某些应用程序,您可能会发现自己处于异步控制流要求过于复杂而无法用 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-saga 非常相似的redux-observable做完全相同的事情。这几乎是一样的,只是生成器和 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 模型 ~ 减速器状态 + 存储)就会触发。
该模式提倡一次对动作排序和模型突变,因为模型的“控制状态”“控制”哪些动作由下一个动作谓词启用和/或自动执行。您根本无法(通常)预测系统在处理操作之前将处于什么状态,因此您的下一个预期操作是否被允许/可能。
因此,例如代码,
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
SAM 不允许,因为可以调度 hideNotification 操作这一事实取决于模型是否成功接受了值“showNotication: true”。模型的其他部分可能会阻止它接受它,因此没有理由触发 hideNotification 操作。
我强烈建议在商店更新后实施适当的下一步操作谓词,并且可以知道模型的新控制状态。这是实现您正在寻找的行为的最安全方法。
如果你愿意,你可以加入我们的 Gitter。这里还有一个SAM 入门指南。
在尝试了各种流行的方法(action creators、thunk、sagas、epics、effects、custom middleware)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,我把我的业务逻辑放在哪里一个 React/Redux 应用程序?
就像这里的讨论一样,我试图对比和比较各种方法。最终,它让我引入了一个新的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 通知示例
我有各种redux-logic jsfiddle 现场示例以及完整示例。我将继续处理文档和示例。
我很想听听您的反馈。
我知道这个问题有点老了,但我将介绍另一个使用redux-observable aka 的解决方案。史诗。
引用官方文档:
什么是 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')
);
应用程序.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 行代码,这太棒了!包括几乎是声明性的事实!
Point 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;
}
我希望它有帮助!
为什么要这么难?这只是 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 问题。因此,它简化了对您的业务逻辑的测试。
测试它是如何实现的似乎没有意义。只有验证通知何时超时才有意义。因此需要存根的代码更少,测试更快,代码更简洁。
如果您想对选择性操作进行超时处理,您可以尝试中间件方法。我在有选择地处理基于承诺的操作时遇到了类似的问题,这个解决方案更加灵活。
假设您的动作创建者如下所示:
//action creator
buildAction = (actionData) => ({
...actionData,
timeout: 500
})
timeout 在上述动作中可以保存多个值
您的中间件实现如下所示:
//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))
你可以在这里找到一些类似的例子
根据 Redux Thunk 文档,执行此操作的适当方法是使用Redux Thunk,它是一种流行的 Redux 中间件:
“Redux Thunk 中间件允许您编写返回函数而不是动作的动作创建者。thunk 可用于延迟动作的调度,或者仅在满足特定条件时调度。内部函数接收存储方法dispatch 和 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 本身是一个非常冗长的库,对于这样的东西,你必须使用像Redux-thunk这样的东西,它会提供一个dispatch
函数,所以你将能够在几秒钟后调度关闭通知。
我创建了一个库来解决冗长和可组合性等问题,您的示例将如下所示:
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],
});
所以我们编写了同步操作来在异步操作中显示通知,它可以在后台请求一些信息,或者稍后检查通知是否被手动关闭。
很简单。使用trim-redux包并在componentDidMount
或其他地方这样写,然后在componentWillUnmount
.
componentDidMount() {
this.tm = setTimeout(function() {
setStore({ age: 20 });
}, 3000);
}
componentWillUnmount() {
clearTimeout(this.tm);
}
这可能有点离题,但我想在这里分享它,因为我只是想在给定超时后从状态中删除警报,即自动隐藏警报/通知。
我最终setTimeout()
在<Alert />
组件内使用,以便它可以调用并调度REMOVE
给定的动作id
。
export function Alert(props: Props) {
useEffect(() => {
const timeoutID = setTimeout(() => {
dispatchAction({
type: REMOVE,
payload: {
id: id,
},
});
}, timeout ?? 2000);
return () => clearTimeout(timeoutID);
}, []);
return <AlertComponent {...props} />;
}