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


889

我有一个操作可以更新我的应用程序的通知状态。通常,此通知将是错误或某种信息。然后,我需要在5秒钟后调度另一项操作,该操作会将通知状态恢复为初始状态,因此没有通知。其背后的主要原因是提供了5秒钟后通知自动消失的功能。

我在使用setTimeout并返回其他动作方面没有运气,也找不到在线完成此操作的方法。因此,欢迎提出任何建议。


30
redux-saga如果您想要比杂项更好的东西,请不要忘记查看我的基本答案。答案较晚,因此您必须滚动很长时间才能看到它:)并不意味着不值得阅读。这是一个捷径:stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
每当您执行setTimeout时,请不要忘记在componentWillUnMount生命周期方法中使用clearTimeout清除计时器
Hemadri Dasari

2
redux-saga很酷,但是它们似乎不支持生成器函数的类型化响应。如果您将带有响应的打字稿使用,可能会很重要。
克里斯蒂安·拉米雷斯

Answers:


2614

不要陷入认为图书馆应该规定如何做所有事情陷阱。如果您想在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作为第一个论点?因为它需要将操作调度到商店。通常,一个组件可以访问,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.')    

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

单例存储也使测试更加困难。在测试操作创建者时,您不再可以模拟商店,因为他们引用了从特定模块导出的特定实际商店。您甚至无法从外部重置其状态。

因此,尽管从技术上讲您可以从模块中导出单例存储,但我们不建议这样做。除非您确定您的应用程序永远不会添加服务器渲染,否则请不要执行此操作。

回到以前的版本:

// 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.')    

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

Thunk中间件

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

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

例如,不幸的是我们不得不过去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)
})

启用此中间件后,如果您调度一个函数,则Redux Thunk中间件会将其dispatch作为参数。它还会“吞噬”这样的动作,因此不必担心您的reducer会收到奇怪的函数参数。减速器将只接收普通对象操作-直接发出或由我们刚刚描述的函数发出。

这看起来不是很有用,是吗?不在这种特殊情况下。但是,它使我们可以声明showNotificationWithTimeout()为常规Redux操作创建者:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

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

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

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

我们将如何在组件中使用它?绝对可以这样写:

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

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

但是,这比原始版本更尴尬!我们为什么还要那样走?

因为我之前告诉过你 如果启用了Redux Thunk中间件,则任何时候您尝试分派一个函数而不是一个操作对象时,中间件都将使用dispatch方法本身作为第一个参数来调用该函数

因此,我们可以改为:

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

最后,分派异步操作(实际上是一系列操作)看起来与向组件同步分派单个操作没有什么不同。这很好,因为组件不必关心是同步还是异步发生。我们只是将其抽象出来。

请注意,由于我们“教” Redux来识别此类“特殊”动作创建者(我们称其为“ 笨拙的动作创建者”),因此我们现在可以在任何使用常规动作创建者的地方使用它们。例如,我们可以将它们用于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),并且想要在某种情况下避免这种情况该怎么办?

无需使用笨拙的中间件,您只需在组件内部进行以下检查:

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

但是,提取动作创建者的目的是将重复性逻辑集中在许多组件上。幸运的是,Redux Thunk为您提供了一种读取 Redux存储库当前状态的方法。除之外dispatch,它还getState作为第二个参数传递给您从重击动作创建者返回的函数。这使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返回的内容,但会为您提供的返回值dispatch()。这就是为什么您可以从thunk中返回Promise并通过调用等待它完成的原因dispatch(someThunkReturningPromise()).then(...)

您也可以将复杂的重击动作创建者分成几个较小的重击动作创建者。dispatchthunk提供的方法本身可以接受thunk,因此您可以递归应用模式。同样,这对Promises效果最好,因为您可以在此之上实现异步控制流。

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

最后,如果您没有真正的需求,请不要使用任何东西(包括音乐)。请记住,根据要求,您的解决方案可能看起来像

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

除非您知道为什么要这么做,否则不要流汗。


27
异步操作似乎是解决常见问题的一种简单而优雅的解决方案。为什么不需要中间件就不支持它们到redux?这样,答案可能会更加简洁。
菲尔·曼德

83
@PhilMander因为有很多替代模式,例如github.com/raisemarketplace/redux-loopgithub.com/yelouafi/redux-saga,它们(如果不是更多的话)同样优雅。Redux是一个低级工具。您可以构建自己喜欢的超集并单独分发。
Dan Abramov

16
您能否解释一下:*考虑将业务逻辑放入化简器*,这是否意味着我应该分派一个动作,然后根据我的状态确定要分派的其他动作?我的问题是,然后我直接在我的reducer中调度其他操作吗?如果不是,那么我从哪里调度它们呢?
froginvasion '16

25
这句话仅适用于同步情况。例如,如果您编写,if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })也许您应该只是dispatch({ type: 'C', something: cond })选择忽略掉减速器中的操作,而是取决于action.something当前状态。
Dan Abramov

29
@DanAbramov您对此表示赞同,“除非您有此问题,请使用语言提供的内容并寻求最简单的解决方案。” 直到我才知道是谁写的!
马特·莱西

188

使用Redux-saga

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

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

一般的想法是Redux-saga提供了ES6生成器解释器,使您可以轻松地编写看起来像同步代码的异步代码(这就是为什么您经常在Redux-saga中发现无限while循环的原因)。不知何故,Redux-saga正在Java语言内部直接构建自己的语言。刚开始学习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)
    ]
}

减速器:

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条通知,而第1条通知消失后,第4条通知将在稍后出现。

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

结论

我的代码不是完美的,但是在生产中运行了几个月的0个错误。Redux-saga和生成器最初有点困难,但是一旦您了解了它们,就可以轻松构建这种系统。

实施更复杂的规则非常容易,例如:

  • 当过多的通知被“排队”时,为每个通知提供更少的显示时间,以便队列大小可以更快地减小。
  • 检测窗口大小的变化,并相应地更改显示的最大通知数(例如,桌面= 3,电话肖像= 2,电话横向= 1)

坦白地说,祝您好运,并与流氓正确地实现这种东西。

请注意,您可以使用redux-observable进行完全相同的操作,这与redux-saga非常相似。这几乎是相同的,并且是生成器和RxJS之间的品味问题。


18
我希望在提出问题时能早些回答您,因为我不能完全同意将Saga副作用库用于这样的业务逻辑。减速器和动作创建者用于状态转换。工作流与状态转换功能不同。工作流会逐步过渡,但过渡本身并不是过渡。Redux + React本身就缺少此功能-这正是Redux Saga如此有用的原因。
阿提克斯

4
谢谢,由于这些原因,我尽力使redux-saga受欢迎:)很少有人认为redux-saga只是thunk的替代品,看不到redux-saga如何实现复杂且分离的工作流
Sebastien Lorber

1
究竟。Actions&Reducers都是状态机的一部分。有时,对于复杂的工作流程,您还需要其他一些东西来协调状态机,而这些状态机不直接属于状态机本身!
Atticus

2
行动:有效载荷/事件过渡到状态。减速器:状态转换功能。组件:反映状态的用户界面。但是缺少一个主要方面-您如何管理许多过渡过程,这些过渡过程都有自己的逻辑,这些逻辑决定接下来要执行的过渡?Redux传奇!
阿提克斯

2
@mrbrdo,如果您仔细阅读我的回答,将会注意到通知超时实际上是通过以下方式处理的yield call(delay,timeoutValue);:它不是相同的API,但是具有相同的效果
Sebastien Lorber

25

具有示例项目的存储库

当前有四个样本项目:

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

接受的答案很棒。

但是缺少一些东西:

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

因此,我创建了Hello Async存储库以添加缺少的内容:

  1. 可运行的项目。您可以下载并运行它们而无需修改。
  2. 提供示例代码以获取更多替代方案:

Redux Saga

接受的答案已经提供了异步代码内联,异步动作生成器和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相比

优点

  • 您不会陷入回调地狱。
  • 您可以轻松测试异步流。
  • 您的行为保持纯正。

缺点

  • 它取决于相对较新的ES6 Generators。

如果上面的代码段不能回答您所有的问题,请参考可运行的项目


23

您可以使用redux-thunk做到这一点。在redux文档中有一个指南,用于诸如setTimeout之类的异步操作。


只是一个快速的后续问题,在使用中间件时applyMiddleware(ReduxPromise, thunk)(createStore),这是如何添加多个中间件(以逗号分隔?)的方法,因为我似乎无法正常工作。
Ilja

1
@Ilja这应该起作用:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier '16

22

我建议也看看SAM模式

SAM模式提倡包括“下一个动作谓词”,其中,一旦模型更新(SAM模型〜减速器状态+存储),就会触发诸如“通知在5秒后自动消失”之类的(自动)动作。

模式提倡一次对动作和模型突变进行排序,因为模型的“控制状态”“控制”动作由下一动作谓词启用和/或自动执行。您根本无法预测(一般而言)在处理操作之前系统将处于什么状态,因此无法预测是否将允许/可能执行您的下一个预期操作。

例如,代码

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

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

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

我强烈建议在商店更新并且可以知道模型的新控制状态之后,实施适当的next-action谓词。这是实现所需行为的最安全方法。

您可以根据需要加入Gitter。这里还有SAM入门指南


到目前为止,我只刮了一下表面,但是已经对SAM模式感到很兴奋。V = S( vm( M.present( A(data) ) ), nap(M))真漂亮。感谢您分享您的想法和经验。我会更深入。

@ftor,谢谢!当我第一次写它时,我有同样的感觉。我已经在生产中使用SAM已有将近一年的时间了,我想不起来我需要一个库来实现SAM(甚至是vdom,尽管我知道何时可以使用它)。只需一行代码即可!SAM生成同构代码,在如何处理异步调用方面没有任何歧义...我想不起来我在哪里,我在做什么?
元编程人员

SAM是真正的软件工程模式(只是使用它生产了Alexa SDK)。它基于TLA +,并试图将令人难以置信的工作的力量带给每个开发人员。SAM纠正了(几乎)每个人数十年来一直在使用的三个近似值:-动作可以操纵应用程序状态-分配等效于变异-没有精确的定义编程步骤是什么(例如a = b * ca步骤,是1 /读取b,c 2 /计算b * c,3 /为a分配了三个不同步骤的结果?
元编程器

20

在尝试了各种流行的方法(动作创建者,重击,萨加斯,史诗,特效,自定义中间件)之后,我仍然觉得也许还有改进的余地,所以我在这篇博客文章“ 我的业务逻辑放在哪里”中记录了自己的旅程。一个React / Redux应用程序?

就像这里的讨论一样,我试图对比和比较各种方法。最终,它使我引入了一个新的库redux-logic,该库从史诗,sagas和自定义中间件中汲取了灵感。

它使您可以拦截用于验证,验证,授权的操作,并提供执行异步IO的方法。

可以简单地声明一些常用功能,例如反跳,限制,取消,并且仅使用最新请求(takeLatest)的响应。redux-logic会包装您的代码,为您提供此功能。

这样您就可以自由地实现自己喜欢的核心业务逻辑。除非您愿意,否则不必使用观测器或生成器。使用函数和回调,promise,异步函数(异步/等待)等。

执行简单的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);
  }
});
    

我的存储库中有一个更高级的通知示例,其工作原理与塞巴斯蒂安·洛伯(Sebastian Lorber)所描述的类似,在该示例中,您可以将显示限制为N个项目,并轮流浏览所有排队的项目。redux-logic通知示例

我有各种redux-logic jsfiddle实时示例以及完整示例。我正在继续研究文档和示例。

我希望听到您的反馈。


我不确定我是否喜欢您的图书馆,但我确实喜欢您的文章!干得好,伙计!您已经做了足够的工作来节省他人的时间。
泰勒·朗

2
我在这里为redux-logic创建了一个示例项目:github.com/tylerlong/hello-async/tree/master/redux-logic 我认为这是一款设计良好的软件,与其他软件相比,我看不出任何主要缺点备择方案。
泰勒·朗

9

我知道这个问题有点老了,但是我将介绍另一个使用redux-observable aka的解决方案。史诗。

引用官方文档:

什么是可观察到的redux?

用于Redux的基于RxJS 5的中间件。撰写和取消异步操作以创建副作用等。

史诗是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')
);

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)

如您所见,解决此问题的关键代码非常容易,唯一看起来与其他答案不同的是函数rootEpic。

要点1.与sagas一样,您必须组合史诗才能获得一个顶级功能,该功能可以接收动作流并返回动作流,因此可以将其与中间件工厂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;
}

希望对您有所帮助!


您能解释一下具体的api方法在做什么switchMap吗?
Dmitri Zaitsev

1
我们在Windows上的React Native应用中使用redux-observable。这是解决复杂,高度异步问题的理想解决方案,并通过其Gitter频道和GitHub问题提供了出色的支持。当然,仅当您遇到了它要解决的确切问题时,额外的复杂性层才值得。
Matt Hargett

8

为什么要这么难?这只是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的关注点。因此,它简化了对业务逻辑的测试。

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


1
这应该是最佳答案。
mmla

6

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

假设您的动作创建者如下所示:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

超时可以在上述操作中包含多个值

  • 以毫秒为单位的数字-特定的超时时间
  • 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))

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


5

正确的方法是使用Redux Thunk,根据Redux Thunk文档,Redux ThunkRedux的流行中间件:

“ 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);
  };
}

4

很简单。使用trim-redux软件包并在componentDidMount或其他地方这样写,然后在中杀死它componentWillUnmount

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

3

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],
});

因此,我们在异步操作中编写了用于显示通知的同步操作,该操作可以在后台请求一些信息,或者稍后检查通知是否手动关闭。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.