如何使用超时调度Redux操作?

Ilj*_*lja 829 javascript timeout redux

我有一个更新我的应用程序的通知状态的操作.通常,此通知将是某种错误或信息.我需要在5秒后发送另一个操作,将通知状态返回到初始状态,因此不会发出通知.这背后的主要原因是提供通知在5秒后自动消失的功能.

我没有运气使用setTimeout和返回另一个动作,也无法找到如何在线完成.所以欢迎任何建议.

Dan*_*mov 2469

不要陷入认为图书馆应该规定如何做所有事情陷阱.如果您想在JavaScript中执行超时操作,则需要使用setTimeout.没有理由认为Redux的行为应该有所不同.

Redux 确实提供了一些处理异步内容的替代方法,但是只有在意识到重复代码太多时才应该使用它们.除非您遇到此问题,否则请使用该语言提供的内容并选择最简单的解决方案.

编写异步代码内联

这是迄今为止最简单的方法.这里没有Redux特有的东西.

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Run Code Online (Sandbox Code Playgroud)

同样,从连接组件内部:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Run Code Online (Sandbox Code Playgroud)

唯一的区别是,在连接组件中,您通常无法访问商店本身,但可以将任一个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)
Run Code Online (Sandbox Code Playgroud)

或者,如果您之前已将它们绑定connect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)
Run Code Online (Sandbox Code Playgroud)

到目前为止,我们还没有使用任何中间件或其他高级概念.

提取异步动作创建器

上述方法在简单的情况下工作正常,但您可能会发现它有一些问题:

  • 它会强制您在要显示通知的任何位置复制此逻辑.
  • 通知没有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)
}
Run Code Online (Sandbox Code Playgroud)

现在组件可以使用showNotificationWithTimeout而无需复制此逻辑或具有不同通知的竞争条件:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    
Run Code Online (Sandbox Code Playgroud)

为什么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.')    
Run Code Online (Sandbox Code Playgroud)

这看起来更简单,但我们不建议采用这种方法.我们不喜欢它的主要原因是因为它迫使商店成为单身人士.这使得实现服务器呈现非常困难.在服务器上,您将希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据.

单身商店也使测试更加困难.在测试动作创建者时,您不能再模拟商店,因为它们引用从特定模块导出的特定实体商店.你甚至无法从外面重置它的状态.

因此,虽然您在技术上可以从模块导出单件商店,但我们不鼓励它.除非您确定您的应用永远不会添加服务器渲染,否则不要这样做.

回到以前的版本:

// 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.')    
Run Code Online (Sandbox Code Playgroud)

这解决了重复逻辑的问题,并使我们免于竞争条件.

Thunk中间件

对于简单的应用程序,该方法应该足够了.如果您对它感到满意,请不要担心中间件.

但是,在较大的应用中,您可能会发现一些不便之处.

例如,我们不得不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)
})
Run Code Online (Sandbox Code Playgroud)

启用此中间件后,如果您调度一个函数,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)
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意该函数与我们在上一节中编写的函数几乎完全相同.但是它不接受dispatch作为第一个参数.相反,它返回一个接受dispatch作为第一个参数的函数.

我们如何在我们的组件中使用它?当然,我们可以这样写:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Run Code Online (Sandbox Code Playgroud)

我们正在调用异步动作创建器来获取想要的内部函数,dispatch然后我们通过dispatch.

然而,这比原始版本更加尴尬!为什么我们甚至走那条路?

因为我之前告诉你的.如果启用了Redux Thunk中间件,则每当您尝试调度函数而不是操作对象时,中间件将使用dispatch方法本身作为第一个参数调用该函数.

所以我们可以这样做:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Run Code Online (Sandbox Code Playgroud)

最后,调度异步操作(实际上是一系列操作)与将同一个操作同步分派给组件没什么不同.这是好事,因为组件不应该关心某些事情是同步发生还是异步发生.我们只是把它抽象出来了.

请注意,由于我们"教导"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)
Run Code Online (Sandbox Code Playgroud)

在Thunks中阅读状态

通常,您的Reducer包含用于确定下一个状态的业务逻辑.但是,只有在调度动作后才会启动减速器.如果您在thunk动作创建者中有副作用(例如调用API),并且您想在某些条件下阻止它,该怎么办?

不使用thunk中间件,你只需在组件内部进行检查:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Run Code Online (Sandbox Code Playgroud)

但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件上.幸运的是,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)
  }
}
Run Code Online (Sandbox Code Playgroud)

不要滥用这种模式.当有缓存数据可用时,它可以避免API调用,但它不是构建业务逻辑的良好基础.如果getState()仅用于有条件地分派不同的操作,请考虑将业务逻辑放入reducers中.

下一步

既然您对thunks如何工作有基本的直觉,请查看使用它们的Redux 异步示例.

你可能会发现许多thunk返回Promises的例子.这不是必需的,但可以非常方便.Redux并不关心你从thunk返回什么,但是它给你的返回值dispatch().这就是为什么你可以从thunk返回一个Promise并等待它通过调用完成dispatch(someThunkReturningPromise()).then(...).

您也可以将复杂的thunk动作创建者分成几个较小的thunk动作创建者.dispatchthunks提供的方法本身可以接受thunk,因此可以递归地应用模式.同样,这最适合Promises,因为您可以在其上实现异步控制流.

对于某些应用程序,您可能会发现自己的异步控制流要求过于复杂而无法用thunk表示.例如,重试失败的请求,带令牌的重新授权流程或逐步入门可能过于冗长且以这种方式编写时容易出错.在这种情况下,您可能希望查看更高级的异步控制流解决方案,例如Redux SagaRedux Loop.评估它们,比较与您的需求相关的示例,并选择您最喜欢的那个.

最后,如果你没有真正的需要,不要使用任何东西(包括thunk).请记住,根据要求,您的解决方案可能看起来很简单

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Run Code Online (Sandbox Code Playgroud)

除非你知道为什么要这样做,否则不要出汗.

  • @PhilMander因为有许多替代模式,如https://github.com/raisemarketplace/redux-loop或https://github.com/yelouafi/redux-saga,它们就像(如果不是更多)优雅一样.Redux是一个低级工具.您可以构建自己喜欢的超集并单独分发. (80认同)
  • @DanAbramov你得到了我的upvote只是为了"除非你有这个问题,使用语言提供的东西,并寻求最简单的解决方案." 只有在我意识到谁写了它之后! (26认同)
  • 对于常见问题,异步操作似乎是一种简单而优雅的解决方案.为什么不支持他们在不需要中间件的情况下使用redux?这个答案可以更加简洁. (24认同)
  • 这句话仅适用于同步案例.例如,如果你写`if(cond)dispatch({type:'A'})else dispatch({type:'B'})`也许你应该只是'dispatch({type:'C',某事:cond} )`并选择忽略reducers中的动作,取决于`action.something`和当前状态. (23认同)
  • 你能解释一下:*考虑将业务逻辑放入reducers*中,这是否意味着我应该发送一个动作,然后在reducer中确定根据我的状态发送什么进一步的动作?我的问题是,我是否直接在我的reducer中发送其他操作,如果没有,那么我从哪里发送它们? (15认同)
  • @DanAbramov这是一个很好的解释,正是文档中缺少的东西!在文档中,你直接寻找thunks,跳过基础为什么不能简单地使用更简单的方法,以及thunk给你的东西.我真的希望文档可以复制这个非常教学的文档,或者至少从FAQ链接到它. (8认同)
  • @DanAbramov再一次展示了Redux设计选择的周到,以及他如何擅长解释它们 - bravo,先生! (5认同)
  • "如果启用了Redux Thunk中间件,那么每次尝试调度函数而不是动作对象时,中间件都会调用该函数,并将调度方法本身作为第一个参数." - 这就是我一段时间的错误,现在它是有道理的 (3认同)
  • 我找到了最全面的Redux,Thunks和Sagas指南.非常感谢您花时间写下来! (2认同)

Seb*_*ber 175

使用Redux-saga

正如Dan Abramov所说,如果你想对异步代码进行更高级的控制,你可以看看redux-saga.

这个答案是一个简单的例子,如果你想更好地解释为什么redux-saga对你的应用程序有用,请查看另一个答案.

一般的想法是Redux-saga提供了一个ES6生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么你经常在Redux-saga中找到无限的while循环).不知何故,Redux-saga正在Javascript中直接构建自己的语言.Redux-saga起初可能感觉有点难学,因为你需要对生成器有基本的了解,但也要理解Redux-saga提供的语言.

我将在这里尝试描述我在redux-saga之上构建的通知系统.此示例目前在生产中运行.

高级通知系统规范

  • 您可以请求显示通知
  • 您可以请求隐藏通知
  • 通知不应超过4秒
  • 可以同时显示多个通知
  • 可以同时显示不超过3个通知
  • 如果在已经显示3个通知的情况下请求通知,则排队/推迟通知.

结果

我的生产应用程序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)
    ]
}
Run Code Online (Sandbox Code Playgroud)

还原剂:

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

用法

您可以简单地调度TOAST_DISPLAY_REQUESTED事件.如果您发送4个请求,则只会显示3个通知,第1个通知消失后,第4个通知会稍后显示.

请注意,我不特别建议TOAST_DISPLAY_REQUESTED从JSX 调度.您宁愿添加另一个可以侦听已存在的应用事件的传奇,然后调度TOAST_DISPLAY_REQUESTED:触发​​通知的组件,不必与通知系统紧密耦合.

结论

我的代码并不完美,但在生产中运行了0个bug几个月.Redux-saga和生成器最初有点难,但是一旦你理解它们,这种系统很容易构建.

实现更复杂的规则甚至很容易,例如:

  • 当太多通知被"排队"时,为每个通知提供更少的显示时间,以便更快地减少队列大小.
  • 检测窗口大小更改,并相应地更改显示的通知的最大数量(例如,桌面= 3,手机肖像= 2,手机格局= 1)

恭顺,祝你好运用thunks实现这种东西.

请注意,你可以使用redux-observable做同样的事情,这与redux-saga非常相似.它几乎是相同的,是发电机和RxJS之间的品味问题.

  • 我希望你的答案在问题被提出时更早出现,因为我不能同意将Saga副作用库用于这样的业务逻辑.Reducers&Action Creators用于状态转换.工作流程与状态转换功能不同.工作流程逐步完成转换,但不是转换本身.Redux + React自己缺乏这一点 - 这正是Redux Saga如此有用的原因. (17认同)
  • 谢谢,我尽力让redux-saga因为这些原因而受欢迎:)很少有人认为目前redux-saga只是thunk的替代品而且看不到redux-saga如何实现复杂和分离的工作流程 (3认同)
  • 确切地。Actions &amp; Reducers 都是状态机的一部分。有时,对于复杂的工作流,您需要一些其他东西来编排状态机,而这不是状态机本身的直接组成部分! (2认同)
  • 操作:有效负载/事件转换状态.减速器:状态转换功能.组件:反映状态的用户界面.但是有一个主要部分缺失 - 你如何管理许多过渡的过程,这些过渡都有自己的逻辑来决定下一步要执行的转换?Redux Saga! (2认同)
  • @mrbrdo如果你仔细阅读我的回答,你会注意到通知超时实际上是用`yield call(delay,timeoutValue)来处理的;`:它不是同一个API,但它具有相同的效果 (2认同)

Tyl*_*ong 23

包含示例项目的存储库

目前有四个示例项目:

  1. 编写异步代码内联
  2. 提取异步动作创建器
  3. 使用Redux Thunk
  4. 使用Redux Saga

接受的答案很棒.

但是缺少一些东西:

  1. 没有可运行的示例项目,只有一些代码片段.
  2. 没有其他替代方案的示例代码,例如:
    1. Redux Saga

所以我创建了Hello Async存储库来添加缺少的东西:

  1. 可运行的项目.您无需修改​​即可下载并运行它们.
  2. 提供更多替代品的示例代码:

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 }
}
Run Code Online (Sandbox Code Playgroud)

行动简单而纯粹.

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)
Run Code Online (Sandbox Code Playgroud)

组件没什么特别之处.

// 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
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

与Redux Thunk相比

优点

  • 你最终没有回调地狱.
  • 您可以轻松地测试异步流程.
  • 你的行为保持纯洁.

缺点

  • 这取决于相对较新的ES6发电机.

如果上面的代码片段没有回答您的所有问题,请参阅runnable项目.


Fat*_*kli 22

你可以用redux-thunk做到这一点.redux文档中有一个指南,用于异步操作,如setTimeout.


小智 21

我建议也看看SAM模式.

SAM模式主张包括"下一个动作谓词",其中一旦模型更新(SAM模型〜减速器状态+存储),其中(自动)动作(例如"通知在5秒后自动消失")被触发.

该模式主张一次一个地对动作和模型突变进行排序,因为模型的"控制状态""控制"由下一个动作谓词启用和/或自动执行哪些动作.您根本无法预测(通常)系统处理操作之前的状态,因此您的下一个预期操作是否允许/可能.

所以例如代码,

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}
Run Code Online (Sandbox Code Playgroud)

不允许使用SAM,因为可以调度hideNotification操作的事实取决于成功接受值"showNotication:true"的模型.模型的其他部分可能会阻止它接受它,因此,没有理由触发hideNotification操作.

我强烈建议在存储更新和模型的新控制状态之后实现适当的下一个动作谓词.这是实现您正在寻找的行为最安全的方式.

如果您愿意,可以加入我们的Gitter.此处还提供了SAM入门指南.


Jef*_*ski 18

尝试各种流行的方法(动作的创造者,的thunk,传奇,史诗,影响,自定义中间件)之后,我仍然觉得,也许有改进的余地,所以我记录我的旅程这篇博客文章中,我应该把我的业务逻辑一个React/Redux应用程序?

就像这里的讨论一样,我试图对比并比较各种方法.最终,它让我引入了一个新的图书馆redux-logic,它从史诗,传奇,自定义中间件中获取灵感.

它允许您拦截操作以验证,验证,授权以及提供执行异步IO的方法.

一些常见功能可以简单地声明为去抖动,限制,取消,并且仅使用来自最新请求(takeLatest)的响应.redux-logic包装您的代码,为您提供此功能.

这使您可以随心所欲地实现核心业务逻辑.除非您愿意,否则不必使用可观察量或生成器.使用函数和回调,promises,异步函数(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);
  }
});
    
Run Code Online (Sandbox Code Playgroud)

我在我的仓库中有一个更高级的通知示例,其工作方式类似于Sebastian Lorber所描述的,您可以将显示限制为N个项目并旋转排队的任何项目.redux-logic通知示例

我有各种redux-logic jsfiddle实例和完整的例子.我将继续研究文档和示例.

我很想听听你的反馈.

  • 我在这里为redux-logic创建了一个示例项目:https://github.com/tylerlong/hello-async/tree/master/redux-logic我认为它是一个设计良好的软件,我没有看到任何重大与其他替代品相比的缺点. (2认同)

Ali*_*eza 8

根据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);
  };
}
Run Code Online (Sandbox Code Playgroud)


cne*_*ans 8

我知道这个问题有点旧,但我将介绍另一种使用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))
)
Run Code Online (Sandbox Code Playgroud)

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')
);
Run Code Online (Sandbox Code Playgroud)

App.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)
Run Code Online (Sandbox Code Playgroud)

解决这个问题的关键代码就像你看到的那样简单,唯一与其他答案不同的是函数rootEpic.

要点1.与传奇一样,你必须结合史诗才能获得一个顶层函数,它接收一系列动作并返回一系列动作,因此你可以将它与中间件工厂createEpicMiddleware一起使用.在我们的例子中,我们只需要一个,所以我们只有rootEpic,所以我们不需要组合任何东西,但这是一个很好的事实.

第2点.我们关注副作用逻辑的rootEpic只需要大约5行代码就可以了!包括几乎是声明的事实!

第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;
}
Run Code Online (Sandbox Code Playgroud)

我希望它有所帮助!


Van*_*uan 6

为什么要这么难?这只是UI逻辑。使用专用操作来设置通知数据:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })
Run Code Online (Sandbox Code Playgroud)

并显示它的专用组件:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,问题应该是“如何清理旧状态?”,“如何通知组件时间已更改”。

您可以实现一些TIMEOUT操作,该操作是在组件的setTimeout上分派的。

也许只要显示新通知就清理它就好了。

无论如何,应该在某个setTimeout地方,对吗?为什么不在组件中这样做

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )
Run Code Online (Sandbox Code Playgroud)

其动机是“通知淡出”功能确实是UI的关注点。因此,它简化了对业务逻辑的测试。

测试它的实现方式似乎没有任何意义。验证通知何时应该超时才有意义。因此,更少的代码存根,更快的测试,更干净的代码。


Yas*_*ash 5

如果要对选择性操作进行超时处理,可以尝试使用中间件方法.我有选择地处理基于承诺的行为遇到了类似的问题,这个解决方案更加灵活.

让我们说你的动作创建者看起来像这样:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})
Run Code Online (Sandbox Code Playgroud)

timeout可以在上面的操作中保存多个值

  • 以ms为单位的数字 - 表示特定的超时时间
  • true - 持续超时持续时间.(在中间件中处理)
  • undefined - 立即发送

您的中间件实现如下所示:

//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)
}
Run Code Online (Sandbox Code Playgroud)

您现在可以使用redux通过此中间件层路由所有操作.

createStore(reducer, applyMiddleware(timeoutMiddleware))
Run Code Online (Sandbox Code Playgroud)

你可以在这里找到一些类似的例子


Blo*_*mca 5

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],
});
Run Code Online (Sandbox Code Playgroud)

所以我们编写同步动作来在异步动作中显示通知,它可以请求一些背景信息,或者稍后检查通知是否被手动关闭。