Javascript 如何在超时时分派 Redux 操作?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/35411423/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to dispatch a Redux action with a timeout?
提问by Ilja
I have an action that updates the notification state of my application. Usually, this notification will be an error or info of some sort. I need to then dispatch another action after 5 seconds that will return the notification state to the initial one, so no notification. The main reason behind this is to provide functionality where notifications disappear automatically after 5 seconds.
我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 5 秒后调度另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在 5 秒后自动消失的功能。
I had no luck with using setTimeout
and returning another action and can't find how this is done online. So any advice is welcome.
我没有使用setTimeout
和返回另一个操作的运气,也找不到在线完成此操作的方式。因此,欢迎任何建议。
回答by Dan Abramov
Don't fall into the trap of thinking a library should prescribe how to do everything. If you want to do something with a timeout in JavaScript, you need to use setTimeout
. There is no reason why Redux actions should be any different.
不要陷入认为图书馆应该规定如何做所有事情的陷阱。如果你想在 JavaScript 中做一些超时的事情,你需要使用setTimeout
. Redux 操作没有任何不同的理由。
Redux doesoffer some alternative ways of dealing with asynchronous stuff, but you should only use those when you realize you are repeating too much code. Unless you have this problem, use what the language offers and go for the simplest solution.
Redux确实提供了一些处理异步内容的替代方法,但是只有当您意识到重复了太多代码时才应该使用这些方法。除非您遇到此问题,否则请使用该语言提供的内容并寻求最简单的解决方案。
Writing Async Code Inline
内联编写异步代码
This is by far the simplest way. And there's nothing specific to Redux here.
这是迄今为止最简单的方法。这里没有任何特定于 Redux 的内容。
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Similarly, from inside a connected component:
类似地,从连接组件内部:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
The only difference is that in a connected component you usually don't have access to the store itself, but get either dispatch()
or specific action creators injected as props. However this doesn't make any difference for us.
唯一的区别是,在连接的组件中,您通常无法访问商店本身,但可以将dispatch()
特定的动作创建者作为道具注入。然而,这对我们来说没有任何区别。
If you don't like making typos when dispatching the same actions from different components, you might want to extract action creators instead of dispatching action objects inline:
如果您不喜欢在从不同组件分派相同操作时犯错,您可能希望提取操作创建者而不是内联分派操作对象:
// 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)
Or, if you have previously bound them with connect()
:
或者,如果您之前已将它们绑定到connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
So far we have not used any middleware or other advanced concept.
到目前为止,我们还没有使用任何中间件或其他高级概念。
Extracting Async Action Creator
提取 Async Action Creator
The approach above works fine in simple cases but you might find that it has a few problems:
上述方法在简单情况下工作正常,但您可能会发现它有一些问题:
- It forces you to duplicate this logic anywhere you want to show a notification.
- The notifications have no IDs so you'll have a race condition if you show two notifications fast enough. When the first timeout finishes, it will dispatch
HIDE_NOTIFICATION
, erroneously hiding the second notification sooner than after the timeout.
- 它迫使您在任何想要显示通知的地方复制此逻辑。
- 通知没有 ID,因此如果您足够快地显示两个通知,您就会遇到竞争条件。当第一个超时完成时,它会 dispatch
HIDE_NOTIFICATION
,错误地比超时后更早地隐藏第二个通知。
To solve these problems, you would need to extract a function that centralizes the timeout logic and dispatches those two actions. It might look like this:
要解决这些问题,您需要提取一个函数来集中超时逻辑并分派这两个操作。它可能看起来像这样:
// 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)
}
Now components can use showNotificationWithTimeout
without duplicating this logic or having race conditions with different notifications:
现在组件可以使用showNotificationWithTimeout
而无需复制此逻辑或具有不同通知的竞争条件:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Why does showNotificationWithTimeout()
accept dispatch
as the first argument? Because it needs to dispatch actions to the store. Normally a component has access to dispatch
but since we want an external function to take control over dispatching, we need to give it control over dispatching.
为什么showNotificationWithTimeout()
接受dispatch
作为第一个参数?因为它需要将操作分派到商店。通常一个组件可以访问,dispatch
但由于我们想要一个外部函数来控制分派,我们需要让它控制分派。
If you had a singleton store exported from some module, you could just import it and dispatch
directly on it instead:
如果你有一个从某个模块导出的单例存储,你可以直接导入它,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.')
This looks simpler but we don't recommend this approach. The main reason we dislike it is because it forces store to be a singleton. This makes it very hard to implement server rendering. On the server, you will want each request to have its own store, so that different users get different preloaded data.
这看起来更简单,但我们不推荐这种方法。我们不喜欢它的主要原因是因为它强制 store 成为单例。这使得实现服务器渲染变得非常困难。在服务器上,您会希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。
A singleton store also makes testing harder. You can no longer mock a store when testing action creators because they reference a specific real store exported from a specific module. You can't even reset its state from outside.
单例存储也使测试更加困难。在测试动作创建器时,您不能再模拟商店,因为它们引用从特定模块导出的特定真实商店。您甚至无法从外部重置其状态。
So while you technically can export a singleton store from a module, we discourage it. Don't do this unless you are sure that your app will never add server rendering.
因此,虽然您在技术上可以从模块导出单例存储,但我们不鼓励这样做。除非您确定您的应用程序永远不会添加服务器渲染,否则不要这样做。
Getting back to the previous version:
回到之前的版本:
// 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.')
This solves the problems with duplication of logic and saves us from race conditions.
这解决了逻辑重复的问题,并使我们免于竞争条件。
Thunk Middleware
Thunk 中间件
For simple apps, the approach should suffice. Don't worry about middleware if you're happy with it.
对于简单的应用程序,这种方法应该足够了。如果您对中间件感到满意,请不要担心它。
In larger apps, however, you might find certain inconveniences around it.
但是,在较大的应用程序中,您可能会发现它存在某些不便之处。
For example, it seems unfortunate that we have to pass dispatch
around. This makes it trickier to separate container and presentational componentsbecause any component that dispatches Redux actions asynchronously in the manner above has to accept dispatch
as a prop so it can pass it further. You can't just bind action creators with connect()
anymore because showNotificationWithTimeout()
is not really an action creator. It does not return a Redux action.
例如,我们不得不dispatch
绕过似乎很不幸。这使得分离容器和展示组件变得更加棘手,因为任何以上述方式异步调度 Redux 操作的组件都必须接受dispatch
作为一个 prop 才能进一步传递它。你不能再绑定动作创建者,connect()
因为showNotificationWithTimeout()
它不是真正的动作创建者。它不返回 Redux 操作。
In addition, it can be awkward to remember which functions are synchronous action creators like showNotification()
and which are asynchronous helpers like showNotificationWithTimeout()
. You have to use them differently and be careful not to mistake them with each other.
此外,记住哪些函数是同步动作创建器showNotification()
,哪些是异步助手,如showNotificationWithTimeout()
. 您必须以不同的方式使用它们,并小心不要将它们相互混淆。
This was the motivation for finding a way to “legitimize” this pattern of providing dispatch
to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creatorsrather than totally different functions.
这是寻找一种方法来“合法化”这种提供dispatch
给辅助函数的模式的动机,并帮助 Redux“看到”这样的异步动作创建者作为正常动作创建者的特例,而不是完全不同的功能。
If you're still with us and you also recognize as a problem in your app, you are welcome to use the Redux Thunkmiddleware.
如果您仍然与我们在一起并且您也发现您的应用程序中存在问题,欢迎您使用Redux Thunk中间件。
In a gist, Redux Thunk teaches Redux to recognize special kinds of actions that are in fact functions:
总而言之,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)
})
When this middleware is enabled, if you dispatch a function, Redux Thunk middleware will give it dispatch
as an argument. It will also “swallow” such actions so don't worry about your reducers receiving weird function arguments. Your reducers will only receive plain object actions—either emitted directly, or emitted by the functions as we just described.
启用此中间件后,如果您调度一个 function,Redux Thunk 中间件会将其dispatch
作为参数。它也会“吞下”这样的动作,所以不要担心你的减速器接收到奇怪的函数参数。你的 reducer 只会接收普通的对象动作——直接发出,或者由我们刚刚描述的函数发出。
This does not look very useful, does it? Not in this particular situation. However it lets us declare showNotificationWithTimeout()
as a regular Redux action creator:
这看起来不是很有用,是吗?不是在这种特殊情况下。然而,它让我们声明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)
}
}
Note how the function is almost identical to the one we wrote in the previous section. However it doesn't accept dispatch
as the first argument. Instead it returnsa function that accepts dispatch
as the first argument.
请注意该函数与我们在上一节中编写的函数几乎相同。但是它不接受dispatch
作为第一个参数。相反,它返回一个接受dispatch
作为第一个参数的函数。
How would we use it in our component? Definitely, we could write this:
我们将如何在我们的组件中使用它?当然,我们可以这样写:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
We are calling the async action creator to get the inner function that wants just dispatch
, and then we pass dispatch
.
我们正在调用异步动作创建者来获取想要的内部函数,dispatch
然后我们通过dispatch
.
However this is even more awkward than the original version! Why did we even go that way?
然而,这比原始版本更尴尬!我们为什么要走那条路?
Because of what I told you before. If Redux Thunk middleware is enabled, any time you attempt to dispatch a function instead of an action object, the middleware will call that function with dispatch
method itself as the first argument.
因为我之前告诉你的。如果启用了 Redux Thunk 中间件,则任何时候您尝试调度函数而不是操作对象时,中间件都会调用该函数,并将dispatch
方法本身作为第一个参数。
So we can do this instead:
所以我们可以这样做:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Finally, dispatching an asynchronous action (really, a series of actions) looks no different than dispatching a single action synchronously to the component. Which is good because components shouldn't care whether something happens synchronously or asynchronously. We just abstracted that away.
最后,分派异步操作(实际上,一系列操作)与将单个操作同步分派到组件看起来没有什么不同。这很好,因为组件不应该关心某些事情是同步发生还是异步发生。我们只是把它抽象出来。
Notice that since we “taught” Redux to recognize such “special” action creators (we call them thunkaction creators), we can now use them in any place where we would use regular action creators. For example, we can use them with connect()
:
请注意,由于我们“教”了 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)
Reading State in Thunks
Thunks 中的读取状态
Usually your reducers contain the business logic for determining the next state. However, reducers only kick in after the actions are dispatched. What if you have a side effect (such as calling an API) in a thunk action creator, and you want to prevent it under some condition?
通常,您的减速器包含用于确定下一个状态的业务逻辑。然而,reducer 只在动作被分派后才开始。如果您在 thunk 动作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它怎么办?
Without using the thunk middleware, you'd just do this check inside the component:
不使用 thunk 中间件,您只需在组件内部执行此检查:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
However, the point of extracting an action creator was to centralize this repetitive logic across many components. Fortunately, Redux Thunk offers you a way to readthe current state of the Redux store. In addition to dispatch
, it also passes getState
as the second argument to the function you return from your thunk action creator. This lets the thunk read the current state of the store.
然而,提取动作创建者的目的是将这种重复的逻辑集中在许多组件中。幸运的是,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)
}
}
Don't abuse this pattern. It is good for bailing out of API calls when there is cached data available, but it is not a very good foundation to build your business logic upon. If you use getState()
only to conditionally dispatch different actions, consider putting the business logic into the reducers instead.
不要滥用这种模式。当有可用的缓存数据时,它有助于避免 API 调用,但它不是构建业务逻辑的良好基础。如果您getState()
仅用于有条件地调度不同的操作,请考虑将业务逻辑放入减速器中。
Next Steps
下一步
Now that you have a basic intuition about how thunks work, check out Redux async examplewhich uses them.
现在您对 thunk 的工作方式有了基本的了解,请查看使用它们的Redux async 示例。
You may find many examples in which thunks return Promises. This is not required but can be very convenient. Redux doesn't care what you return from a thunk, but it gives you its return value from dispatch()
. This is why you can return a Promise from a thunk and wait for it to complete by calling dispatch(someThunkReturningPromise()).then(...)
.
你可能会发现很多 thunk 返回 Promise 的例子。这不是必需的,但可能非常方便。Redux 并不关心你从 thunk 返回什么,但它会从dispatch()
. 这就是为什么您可以从 thunk 返回一个 Promise 并通过调用dispatch(someThunkReturningPromise()).then(...)
.
You may also split complex thunk action creators into several smaller thunk action creators. The dispatch
method provided by thunks can accept thunks itself, so you can apply the pattern recursively. Again, this works best with Promises because you can implement asynchronous control flow on top of that.
您还可以将复杂的 thunk 动作创建器拆分为几个较小的 thunk 动作创建器。dispatch
thunk 提供的方法可以接受 thunk 本身,因此您可以递归地应用该模式。同样,这对 Promise 最有效,因为您可以在其之上实现异步控制流。
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 Sagaor Redux Loop. Evaluate them, compare the examples relevant to your needs, and pick the one you like the most.
对于某些应用程序,您可能会发现自己的异步控制流要求过于复杂,无法用 thunk 来表达。例如,重试失败的请求、使用令牌重新授权流程或分步引导在以这种方式编写时可能过于冗长且容易出错。在这种情况下,您可能需要查看更高级的异步控制流解决方案,例如Redux Saga或Redux Loop。评估它们,比较与您的需求相关的示例,然后选择您最喜欢的示例。
Finally, don't use anything (including thunks) if you don't have the genuine need for them. Remember that, depending on the requirements, your solution might look as simple as
最后,如果您真的不需要它们,请不要使用任何东西(包括 thunk)。请记住,根据要求,您的解决方案可能看起来很简单
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Don't sweat it unless you know why you're doing this.
除非你知道你为什么这样做,否则不要出汗。
回答by Sebastien Lorber
Using Redux-saga
使用 Redux-saga
As Dan Abramov said, if you want more advanced control over your async code, you might take a look at redux-saga.
正如 Dan Abramov 所说,如果你想对异步代码进行更高级的控制,你可以看看redux-saga。
This answer is a simple example, if you want better explanations on why redux-saga can be useful for your application, check this other answer.
这个答案是一个简单的例子,如果您想更好地解释为什么 redux-saga 对您的应用程序有用,请查看其他答案。
The general idea is that Redux-saga offers an ES6 generators interpreter that permits you to easily write async code that looks like synchronous code (this is why you'll often find infinite while loops in Redux-saga). Somehow, Redux-saga is building its own language directly inside Javascript. Redux-saga can feel a bit difficult to learn at first, because you need basic understanding of generators, but also understand the language offered by Redux-saga.
总体思路是 Redux-saga 提供了一个 ES6 生成器解释器,它允许您轻松编写看起来像同步代码的异步代码(这就是为什么您经常会在 Redux-saga 中发现无限循环)。不知何故,Redux-saga 直接在 Javascript 中构建了自己的语言。Redux-saga 一开始可能会觉得有点难学,因为你需要对生成器有基本的了解,还要了解 Redux-saga 提供的语言。
I'll try here to describe here the notification system I built on top of redux-saga. This example currently runs in production.
我将尝试在这里描述我在 redux-saga 之上构建的通知系统。此示例目前在生产中运行。
Advanced notification system specification
高级通知系统规范
- You can request a notification to be displayed
- You can request a notification to hide
- A notification should not be displayed more than 4 seconds
- Multiple notifications can be displayed at the same time
- No more than 3 notifications can be displayed at the same time
- If a notification is requested while there are already 3 displayed notifications, then queue/postpone it.
- 您可以请求显示通知
- 您可以请求隐藏的通知
- 通知的显示时间不应超过 4 秒
- 可以同时显示多个通知
- 不能同时显示超过 3 个通知
- 如果在已显示 3 个通知时请求通知,则将其排队/推迟。
Result
结果
Screenshot of my production app Stample.co
我的生产应用程序Stample.co 的屏幕截图
Code
代码
Here I named the notification a toast
but this is a naming detail.
在这里,我将通知命名为 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)
]
}
And the reducer:
和减速机:
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;
}
};
Usage
用法
You can simply dispatch TOAST_DISPLAY_REQUESTED
events. If you dispatch 4 requests, only 3 notifications will be displayed, and the 4th one will appear a bit later once the 1st notification disappears.
您可以简单地调度TOAST_DISPLAY_REQUESTED
事件。如果你发送 4 个请求,只会显示 3 个通知,第 4 个会在第一个通知消失后出现。
Note that I don't specifically recommend dispatching TOAST_DISPLAY_REQUESTED
from JSX. You'd rather add another saga that listens to your already-existing app events, and then dispatch the TOAST_DISPLAY_REQUESTED
: your component that triggers the notification, does not have to be tightly coupled to the notification system.
请注意,我不特别推荐TOAST_DISPLAY_REQUESTED
从 JSX调度。您宁愿添加另一个 saga 来侦听您已经存在的应用程序事件,然后分派TOAST_DISPLAY_REQUESTED
: 触发通知的组件,不必与通知系统紧密耦合。
Conclusion
结论
My code is not perfect but runs in production with 0 bugs for months. Redux-saga and generators are a bit hard initially but once you understand them this kind of system is pretty easy to build.
我的代码并不完美,但在生产环境中运行了几个月,并且有 0 个错误。Redux-saga 和生成器最初有点难,但是一旦你理解了它们,这种系统就很容易构建。
It's even quite easy to implement more complex rules, like:
实现更复杂的规则甚至很容易,例如:
- when too many notifications are "queued", give less display-time for each notification so that the queue size can decrease faster.
- detect window size changes, and change the maximum number of displayed notifications accordingly (for example, desktop=3, phone portrait = 2, phone landscape = 1)
- 当太多通知被“排队”时,为每个通知提供更少的显示时间,以便队列大小可以更快地减少。
- 检测窗口大小变化,并相应地更改显示通知的最大数量(例如,桌面=3,手机纵向=2,手机横向=1)
Honnestly, good luck implementing this kind of stuff properly with thunks.
老实说,祝你好运用 thunk 正确地实现这种东西。
Note you can do exactly the same kind of thing with redux-observablewhich is very similar to redux-saga. It's almost the same and is a matter of taste between generators and RxJS.
请注意,您可以使用redux-observable做完全相同的事情,它与 redux-saga 非常相似。它几乎相同,是生成器和 RxJS 之间的品味问题。
回答by Tyler Long
A repository with sample projects
包含示例项目的存储库
Current there are four sample projects:
目前有四个示例项目:
The accepted answer is awesome.
接受的答案很棒。
But there is something missing:
但是还缺少一些东西:
- No runnable sample projects, just some code snippets.
- No sample code for other alternatives, such as:
- 没有可运行的示例项目,只有一些代码片段。
- 没有其他替代方案的示例代码,例如:
So I created the Hello Asyncrepository to add the missing things:
所以我创建了Hello Async存储库来添加缺少的东西:
- Runnable projects. You can download and run them without modification.
- Provide sample code for more alternatives:
- Redux Saga
- Redux Loop
- ...
- 可运行的项目。您无需修改即可下载并运行它们。
- 提供更多替代方案的示例代码:
- Redux 传奇
- Redux 循环
- ...
Redux Saga
Redux 传奇
The accepted answer already provides sample code snippets for Async Code Inline, Async Action Generator and Redux Thunk. For the sake of completeness, I provide code snippets for Redux Saga:
已接受的答案已经为 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 }
}
Actions are simple and pure.
动作简单而纯粹。
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(
mapStateToProps,
{ showNotificationWithTimeout }
)(MyComponent)
Nothing is special with component.
组件没有什么特别之处。
// 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 are based on ES6 Generators
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)
Compared to Redux Thunk
与 Redux Thunk 相比
Pros
优点
- You don't end up in callback hell.
- You can test your asynchronous flows easily.
- Your actions stay pure.
- 你不会最终陷入回调地狱。
- 您可以轻松测试异步流。
- 你的行为保持纯洁。
Cons
缺点
- It depends on ES6 Generators which is relatively new.
- 它依赖于相对较新的 ES6 Generators。
Please refer to the runnable projectif the code snippets above don't answer all of your questions.
如果上面的代码片段没有回答您的所有问题,请参阅可运行项目。
回答by Fatih Erikli
You can do this with redux-thunk. There is a guide in redux documentfor async actions like setTimeout.
你可以用redux-thunk做到这一点。redux 文档中有一个关于 setTimeout 等异步操作的指南。
回答by Jean-Jacques Dubray
I would recommend also taking a look at the SAM pattern.
我还建议您查看SAM 模式。
The SAM pattern advocates for including a "next-action-predicate" where (automatic) actions such as "notifications disappear automatically after 5 seconds" are triggered once the model has been updated (SAM model ~ reducer state + store).
SAM 模式提倡包含一个“下一个动作谓词”,其中(自动)动作,例如“通知在 5 秒后自动消失”,一旦模型更新(SAM 模型~reducer 状态 + 存储)就会触发。
The pattern advocates for sequencing actions and model mutations one at a time, because the "control state" of the model "controls" which actions are enabled and/or automatically executed by the next-action predicate. You simply cannot predict (in general) what state the system will be prior to processing an action and hence whether your next expected action will be allowed/possible.
该模式提倡一次一个对动作和模型突变进行排序,因为模型的“控制状态”“控制”下一个动作谓词启用和/或自动执行哪些动作。您根本无法预测(通常)系统在处理操作之前将处于什么状态,因此您的下一个预期操作是否会被允许/可能。
So for instance the code,
所以例如代码,
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
would not be allowed with SAM, because the fact that a hideNotification action can be dispatched is dependent on the model successfully accepting the value "showNotication: true". There could be other parts of the model that prevents it from accepting it and therefore, there would be no reason to trigger the hideNotification action.
SAM 不允许,因为可以调度 hideNotification 操作的事实取决于模型成功接受值“showNotification: true”。模型的其他部分可能会阻止它接受它,因此没有理由触发 hideNotification 操作。
I would highly recommend that implement a proper next-action predicate after the store updates and the new control state of the model can be known. That's the safest way to implement the behavior you are looking for.
我强烈建议在商店更新并且可以知道模型的新控制状态之后实施适当的下一步操作谓词。这是实现您正在寻找的行为的最安全方法。
You can join us on Gitter if you'd like. There is also a SAM getting started guide available here.
如果你愿意,你可以在 Gitter 上加入我们。这里还有一份SAM 入门指南。
回答by Jeff Barczewski
After trying the various popular approaches (action creators, thunks, sagas, epics, effects, custom middleware), I still felt that maybe there was room for improvement so I documented my journey in this blog article, Where do I put my business logic in a React/Redux application?
在尝试了各种流行的方法(动作创建器、thunks、sagas、史诗、效果、自定义中间件)后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,我把我的业务逻辑放在哪里React/Redux 应用程序?
Much like the discussions here, I tried to contrast and compare the various approaches. Eventually it led me to introducing a new library redux-logicwhich takes inspiration from epics, sagas, custom middleware.
就像这里的讨论一样,我试图对比和比较各种方法。最终它让我引入了一个新的库redux-logic,它从史诗、传奇、自定义中间件中汲取灵感。
It allows you to intercept actions to validate, verify, authorize, as well as providing a way to perform async IO.
它允许您拦截验证、验证、授权的操作,并提供一种执行异步 IO 的方法。
Some common functionality can simply be declared like debouncing, throttling, cancellation, and only using the response from the latest request (takeLatest). redux-logic wraps your code providing this functionality for you.
一些常用功能可以简单地声明为去抖动、节流、取消,并且仅使用来自最新请求 (takeLatest) 的响应。redux-logic 包装您的代码,为您提供此功能。
That frees you to implement your core business logic however you like. You don't have to use observables or generators unless you want to. Use functions and callbacks, promises, async functions (async/await), etc.
这使您可以随意实现核心业务逻辑。除非您愿意,否则您不必使用 observable 或生成器。使用函数和回调、promise、异步函数(async/await)等。
The code for doing a simple 5s notification would be something like:
执行简单 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);
}
});
I have a more advanced notification example in my repo that works similar to what Sebastian Lorber described where you could limit the display to N items and rotate through any that queued up. redux-logic notification example
我在我的 repo 中有一个更高级的通知示例,它的工作原理类似于 Sebastian Lorber 所描述的,您可以将显示限制为 N 个项目并轮换任何排队的项目。redux-logic 通知示例
I have a variety of redux-logic jsfiddle live examples as well as full examples. I'm continuing to work on docs and examples.
我有各种redux-logic jsfiddle live examples 以及 full examples。我将继续研究文档和示例。
I'd love to hear your feedback.
我很想听听您的反馈。
回答by cnexans
I understand that this question is a bit old but I'm going to introduce another solution using redux-observableaka. Epic.
我知道这个问题有点老了,但我将使用redux-observableaka介绍另一个解决方案。史诗。
Quoting the official documentation:
引用官方文档:
What is redux-observable?
什么是 redux-observable?
RxJS 5-based middleware for Redux. Compose and cancel async actions to create side effects and more.
基于 RxJS 5 的 Redux 中间件。编写和取消异步操作以创建副作用等。
An Epic is the core primitive of redux-observable.
Epic 是 redux-observable 的核心原语。
It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.
它是一个接受动作流并返回动作流的函数。动作进去,动作出来。
In more or less words, you can create a function that receives actions through a Stream and then return a new stream of actions (using common side effects such as timeouts, delays, intervals, and requests).
或多或少地说,您可以创建一个函数,通过 Stream 接收动作,然后返回一个新的动作流(使用常见的副作用,例如超时、延迟、间隔和请求)。
Let me post the code and then explain a bit more about it
让我贴出代码,然后再解释一下
store.js
商店.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
索引.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
应用程序.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)
The key code to solve this problem is as easy as pie as you can see, the only thing that appears different from the other answers is the function rootEpic.
解决这个问题的关键代码就像你看到的一样简单,唯一与其他答案不同的是函数 rootEpic。
Point 1. As with sagas, you have to combine the epics in order to get a top level function that receives a stream of actions and returns a stream of actions, so you can use it with the middleware factory createEpicMiddleware. In our case we only need one so we only have our rootEpicso we don't have to combine anything but it's a good to know fact.
要点 1. 与 saga 一样,您必须组合史诗才能获得接收动作流并返回动作流的顶级函数,因此您可以将其与中间件工厂createEpicMiddleware 一起使用。在我们的例子中,我们只需要一个,所以我们只有rootEpic,所以我们不必组合任何东西,但知道事实是一件好事。
Point 2. Our rootEpicwhich takes care about the side effects logic only takes about 5 lines of code which is awesome! Including the fact that is pretty much declarative!
第 2 点。我们负责副作用逻辑的rootEpic只需要大约 5 行代码,非常棒!包括几乎是声明性的事实!
Point 3. Line by line rootEpic explanation (in comments)
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;
}
I hope it helps!
我希望它有帮助!
回答by Vanuan
Why should it be so hard? It's just UI logic. Use a dedicated action to set notification data:
为什么要这么难?这只是UI逻辑。使用专用操作来设置通知数据:
dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })
and a dedicated component to display it:
和一个专门的组件来显示它:
const Notifications = ({ notificationData }) => {
if(notificationData.expire > this.state.currentTime) {
return <div>{notificationData.message}</div>
} else return null;
}
In this case the questions should be "how do you clean up old state?", "how to notify a component that time has changed"
在这种情况下,问题应该是“你如何清理旧状态?”、“如何通知组件时间已经改变”
You can implement some TIMEOUT action which is dispatched on setTimeout from a component.
您可以实现一些 TIMEOUT 操作,该操作在组件的 setTimeout 上分派。
Maybe it's just fine to clean it whenever a new notification is shown.
也许每当显示新通知时清理它就可以了。
Anyway, there should be some setTimeout
somewhere, right? Why not to do it in a component
无论如何,应该有一些setTimeout
地方,对吧?为什么不在组件中做
setTimeout(() => this.setState({ currentTime: +new Date()}),
this.props.notificationData.expire-(+new Date()) )
The motivation is that the "notification fade out" functionality is really a UI concern. So it simplifies testing for your business logic.
动机是“通知淡出”功能确实是一个 UI 问题。因此,它简化了对业务逻辑的测试。
It doesn't seem to make sense to test how it's implemented. It only makes sense to verify when the notification should time out. Thus less code to stub, faster tests, cleaner code.
测试它是如何实现的似乎没有意义。只有验证通知何时超时才有意义。因此需要存根的代码更少,测试速度更快,代码更干净。
回答by Yash
If you want timeout handling on selective actions, you can try the middlewareapproach. I faced a similar problem for handling promise based actions selectively and this solution was more flexible.
如果您想对选择性操作进行超时处理,您可以尝试中间件方法。我在有选择地处理基于承诺的操作时遇到了类似的问题,这个解决方案更灵活。
Lets say you your action creator looks like this:
假设您的动作创建者如下所示:
//action creator
buildAction = (actionData) => ({
...actionData,
timeout: 500
})
timeout can hold multiple values in the above action
timeout 可以在上述操作中保存多个值
- number in ms - for a specific timeout duration
- true - for a constant timeout duration. (handled in the middleware)
- undefined - for immediate dispatch
- 以毫秒为单位的数字 - 对于特定的超时持续时间
- true - 对于恒定的超时持续时间。(在中间件中处理)
- 未定义 - 立即派送
Your middleware implementation would look like this:
您的中间件实现如下所示:
//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)
}
You can now route all your actions through this middleware layer using redux.
您现在可以使用 redux 通过此中间件层路由所有操作。
createStore(reducer, applyMiddleware(timeoutMiddleware))
You can find some similar examples here
你可以在这里找到一些类似的例子
回答by Alireza
The appropriate way to do this is using Redux Thunkwhich is a popular middleware for Redux, as per Redux Thunk documentation:
根据Redux Thunk 文档,适当的方法是使用Redux Thunk,它是Redux的流行中间件:
"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".
"Redux Thunk 中间件允许你编写返回函数而不是动作的动作创建器。thunk 可用于延迟动作的分派,或者仅在满足特定条件时才分派。内部函数接收存储方法dispatch 和 getState 作为参数”。
So basically it returns a function, and you can delay your dispatch or put it in a condition state.
所以基本上它返回一个函数,您可以延迟调度或将其置于条件状态。
So something like this is going to do the job for you:
所以像这样的事情会为你做这项工作:
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);
};
}