协慌网

登录 贡献 社区

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

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

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

答案

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

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

编写异步代码内联

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

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

同样,从连接组件内部:

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

唯一的区别是,在连接组件中,您通常无法访问商店本身,但可以将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作为 prop,以便它可以进一步传递它。你不能再仅仅使用connect()绑定动作创建者,因为showNotificationWithTimeout()实际上不是动作创建者。它不会返回 Redux 操作。

另外,记住哪些函数是showNotification()等同步动作创建者以及showNotification()类的异步助手可能showNotificationWithTimeout() 。你必须以不同的方式使用它们,并注意不要互相误解。

这是寻找一种方法来 “合理化” 这种向辅助函数提供dispatch的方式的动机,并帮助 Redux“看到” 这样的异步动作创建者作为正常动作创建者的特例而不是完全不同的函数。

如果您仍然和我们在一起,并且您也认为应用程序存在问题,欢迎您使用Redux Thunk中间件。

在一个要点中,Redux Thunk 教 Redux 识别实际上具有功能的特殊动作:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

当启用这个中间件, 如果你发送的功能 ,终极版咚中间件将会给它dispatch作为参数。它也会 “吞下” 这样的动作,所以不要担心你的减速器接收奇怪的函数参数。您的 Reducer 将只接收普通对象操作 - 直接发出,或者由我们刚刚描述的函数发出。

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

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

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

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

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

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

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

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

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

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

所以我们可以这样做:

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

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

请注意,由于我们 “教导”Redux 识别出这样的 “特殊” 动作创建者(我们称之为thunk动作创建者),我们现在可以在任何我们使用常规动作创建者的地方使用它们。例如,我们可以将它们与connect()

// actions.js

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

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

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

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

在 Thunks 中阅读状态

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

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

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

但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件上。幸运的是,Redux Thunk 为您提供了一种读取 Redux 商店当前状态的方法。除了dispatch ,它还将getState作为第二个参数传递给您从 thunk action creator 返回的函数。这让 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()来有条件地分派不同的操作,请考虑将业务逻辑放入 reducers 中。

下一步

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

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

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

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

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

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

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

使用 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 的屏幕截图

祝酒词

在这里,我将通知命名为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 个通知会稍后显示。

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

结论

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

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

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

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

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

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