我有一个更新应用程序通知状态的操作。通常,该通知将是一个错误或某种类型的信息。然后,我需要在5秒后分派另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。
我没有运气使用setTimeout和返回另一个动作,无法找到这是如何在线完成的。所以任何建议都是欢迎的。
我有一个更新应用程序通知状态的操作。通常,该通知将是一个错误或某种类型的信息。然后,我需要在5秒后分派另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。
我没有运气使用setTimeout和返回另一个动作,无法找到这是如何在线完成的。所以任何建议都是欢迎的。
当前回答
在尝试了各种流行的方法(动作创造者,坦克,传奇,史诗,效果,自定义中间件)后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,在React/Redux应用程序中我应该把我的业务逻辑放在哪里?
就像这里的讨论一样,我尝试对比和比较各种方法。最终,我引入了一个新的redux-logic库,它的灵感来自史诗、传奇故事和自定义中间件。
它允许您拦截验证、验证、授权的操作,并提供了一种执行异步IO的方法。
一些常见的功能可以简单地声明,如debashing、节流、取消,并且只使用来自最新请求的响应(takeLatest)。Redux-logic包装为您提供此功能的代码。
这使您可以随心所欲地实现核心业务逻辑。除非你愿意,否则你不必使用可观察对象或生成器。使用函数和回调,承诺,异步函数(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本身是一个非常冗长的库,对于这样的东西,你必须使用像Redux-thunk这样的东西,它会提供一个分派函数,所以你将能够在几秒钟后分派关闭通知。
我已经创建了一个库来解决诸如冗长性和可组合性等问题,您的示例将如下所示:
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],
});
因此,我们在异步动作中组合同步动作来显示通知,这可以请求一些后台信息,或者稍后检查通知是否被手动关闭。
你可以用redux-thunk做到这一点。redux文档中有关于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()或特定的操作创建者注入作为道具。然而,这对我们来说没有任何区别。
如果你不喜欢在从不同组件分派相同的动作时出现错别字,你可能想要提取动作创建者,而不是内联分派动作对象:
// 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.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操作的组件都必须接受分派作为道具,以便进一步传递它。你不能再用connect()绑定动作创建者了,因为showNotificationWithTimeout()并不是一个真正的动作创建者。它不返回Redux操作。
此外,很难记住哪些函数是同步操作创建者(如showNotification()),哪些是异步帮助器(如showNotificationWithTimeout())。你必须以不同的方式使用它们,小心不要把它们弄错。
这就是寻找一种方法来“合法化”这种向helper函数提供分派的模式的动机,并帮助Redux“将”这种异步操作创建者视为普通操作创建者的特殊情况,而不是完全不同的函数。
如果你仍然和我们在一起,你也意识到在你的应用程序中的一个问题,欢迎你使用Redux坦克中间件。
总的来说,Redux坦克教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坦克中间件会把它作为一个参数分派。它也会“吞下”这样的动作,所以不用担心你的约简会收到奇怪的函数参数。您的约简器将只接收普通对象操作——要么直接发出,要么由我们刚才描述的函数发出。
这看起来不是很有用,不是吗?不是在这种特殊情况下。然而,它允许我们声明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创建者来获得内部函数,它只需要分派,然后传递分派。
然而,这比原来的版本更尴尬!我们为什么要走那条路?
因为我之前告诉过你。如果Redux坦克中间件是启用的,任何时候你试图分派一个函数而不是一个动作对象,中间件将调用该函数并将分派方法本身作为第一个参数。
所以我们可以这样做:
// 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)
在坦克中阅读国家
通常,约简器包含用于确定下一个状态的业务逻辑。然而,减量只在行动被分派之后才会起作用。如果你在坦克动作创建器中有副作用(比如调用API),而你想在某些情况下阻止它怎么办?
如果不使用thunk中间件,你只需要在组件内部进行检查:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
然而,提取动作创建器的目的是将这种重复的逻辑集中到许多组件上。幸运的是,Redux坦克为您提供了一种读取Redux存储的当前状态的方法。除了分派,它还将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()来有条件地分派不同的操作,那么可以考虑将业务逻辑放到简化器中。
下一个步骤
现在你已经有了关于坦克如何工作的基本直觉,看看Redux async例子,它使用了它们。
你可能会发现很多例子中,坦克返回承诺。这不是必需的,但非常方便。Redux并不关心你从一个thunk返回什么,但它会给你它从dispatch()返回的值。这就是为什么你可以从一个thunk返回一个Promise,并通过调用dispatch(someThunkReturningPromise()).then(…)来等待它完成。
你也可以把复杂的坦克动作创造者分成几个更小的坦克动作创造者。由thunks提供的分派方法可以接受thunk本身,因此您可以递归地应用该模式。同样,这对于Promises来说效果最好,因为您可以在此基础上实现异步控制流。
For some apps, you may find yourself in a situation where your asynchronous control flow requirements are too complex to be expressed with thunks. For example, retrying failed requests, reauthorization flow with tokens, or a step-by-step onboarding can be too verbose and error-prone when written this way. In this case, you might want to look at more advanced asynchronous control flow solutions such as Redux Saga or Redux Loop. Evaluate them, compare the examples relevant to your needs, and pick the one you like the most.
最后,不要使用任何你没有真正需要的东西(包括坦克)。请记住,根据需求的不同,您的解决方案可能看起来非常简单
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
别担心,除非你知道你为什么这么做。
这很简单。使用trim-redux包,在componentDidMount或其他地方这样写,并在componentWillUnmount中杀死它。
componentDidMount() {
this.tm = setTimeout(function() {
setStore({ age: 20 });
}, 3000);
}
componentWillUnmount() {
clearTimeout(this.tm);
}
如果希望对选择性操作进行超时处理,可以尝试中间件方法。 我遇到过一个类似的问题,有选择地处理基于承诺的行为,这个解决方案更灵活。
假设你的动作创造者是这样的:
//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))
你可以在这里找到一些类似的例子