用redux + redux thunk等待后台任务的习惯方法

时间:2017-02-07 19:39:53

标签: javascript reactjs redux react-redux redux-thunk

在我的应用程序中,我们有一个长期运行的"配置"异步完成的任务,我们必须使用轮询来检测它已完成。因此,在发生这种情况时,用户不会被阻止使用应用程序,我们会在用户交互流程开始时启动它。但是,在交互流程结束时,如果尚未完成,则用户会采取需要等待该配置任务的操作。这是一种序列图来说明。

[provisioning start][poll for completion....]
[user interactions]      [blocked action....][post provisioning task]

我遇到的问题是找出在Redux中正确执行此操作的惯用方法。以下是我考虑过的一些事情:

  1. 有一个"请求" reducer,我为那个长期运行的配置任务存储Promise,然后当[blocked action]执行时,它只是等待承诺。这个问题是Redux特别要求所有商店状态都是 serializable ,所以似乎Promise这样的东西不受欢迎。
  2. 订阅[blocked action]操作创建器中的商店,查找表示配置已完成的数据。不幸的是,我非常喜欢的redux-thunk并不包含对商店的引用,只包含dispatchgetState方法。
  3. 让我的React组件在执行[blocked action]之前等待配置完成。我不喜欢这样,因为它为我的视图层添加了太多动作排序逻辑感知。
  4. 由于这些都不是很好的选择,我现在正在使用:

    1. 将配置Promise存储在模块属性中,基本上执行选项#1而不从商店获取Promise。我对此并不狂热,因为它将状态移到redux循环之外,但它似乎是最简单的选择。
    2. 使用redux-thunk是否有更惯用的方式来实现这一目标?如果有另一个异步中间件使这个更干净,我愿意考虑,虽然我已经投入了redux-thunk

3 个答案:

答案 0 :(得分:1)

有几个redux-promise-middleware选项可让您调度promise承诺,让中间件使用它们,并在promise状态发生变化时发出PENDING,RESOLVED | REJECTED操作。如果您正在使用promises和Redux,您可能需要查看其中一个。但是,他们无法帮助您解决此特定的问题。

我认为redux-saga是一个非常适合帮助您解决此问题的中间件。我还没有亲自使用它,所以我不能提供一个例子。

另一种可能更简单的选择是redux-tap。使用“点按”可显示您的应用可以订阅的操作流。让一个被阻止的动作创建者thunk订阅该流并等待表示该promise已完成的动作(它当然应首先通过getState检查商店的内容,看看在此阻止动作之前是否完成了承诺派出)。像这样:

// ========= configureStore.js
import ee from 'event-emitter';

// ...
export const actionStream = ee();

// ...
const emitActions = tap(({type} => type, (type, action) => actionStream.emit(type, action);

// install emitActions middleware to run *after* thunk (so that it does not see thunk actions but only primitive actions)


// =========== your action module.js
import {actionStream} from './configureStore';

export function blockedAction(arg) {
    return (dispatch, getState) => {
        if (getState().initDone) {
            return dispatch({type: "blockedAction", payload: arg});
        }

        // wait for init action
        actionStream.once("initAction", initAction => dispatch({type: "blockedAction", payload: arg}));
    };
}

请记住,它不是二进制“thunk或xxx中间件”的选择。您可以加载thunk以及许多其他中间件。在一个应用程序中,我使用:

  • 承诺中间件
  • console logging middleware
  • 用于批量更新的自定义中间件
  • 用于将操作持久化到IndexedDB的自定义中间件
  • 用于暂停操作流的自定义中间件
  • 中间件以防止递归商店通知

除了thunk之外没有任何中间件,这是另一种选择:

选项5 - 将您的选项2与您的选项4相结合:在全球范围内存储承诺。在发送原始操作之前,阻止的操作创建者thunk等待promise。要最小化“商店外的状态”,您还可以让promise操作向商店发出操作以保持商店更新(可能使用promise中间件)

答案 1 :(得分:0)

我认为这更简单 - 除非我错过了一些东西 - 如果我们更加严谨地将UI视为国家的一个功能。在交互流程的每一点上都有一些事情是真是假:

  1. 供应已开始/未开始
  2. 配置已完成/未完成
  3. 用户操作正在等待/未挂起
  4. 这三个事实足以告诉我们用户应该在特定时刻在他们的屏幕上看到什么,所以我们的州应该包括以下三个事实:

    const initialState = {
      provisioningStarted: false,
      provisioningDone: false,
      laterActionIsPending: false,
      // ...
    };
    

    使用redux-thunk我们可以处理配置操作:

    function startProvisioning() {
      return dispatch => {
        dispatch({ type: START_PROVISIONING });
    
        pollProvisioning().then(postSetupInfo => {
          dispatch({ type: DONE_PROVISIONING, postSetupInfo });
        });
      };
    }
    

    ......在我们的减速机中:

    function appReducer(state, action) {
      // ...
      switch (action.type) {
        case START_PROVISIONING:
          return { ...state, provisioningStarted: true };
        case DONE_PROVISIONING:
          return { ...state, provisioningDone: true, postSetupInfo: action.postSetupInfo };
        case LATER_ACTION_PENDING:
          return { ...state, laterActionIsPending: true };
        // ...
      }
    }
    

    正如我所说,这应该足以告诉我们用户应该看到的内容:

    const Provisioner = ({provisioningStarted, provisioningDone, /* ... */}) => {
      if (provisioningStarted) {
        if (laterActionIsPending) {
          if (provisioningDone) {
            return <p>Finished! Here's your info: {postSetupInfo}</p>;
          }
          return <p>Provisioning...</p>;
        }
        return <button type="button" onClick={doAction}>Do action</button>;
      }
      return <button type="button" onClick={startProvisioning}>Start provisioning</button>;
    };
    
    export default connect(mapStateToProps, mapDispatchToProps)(Provisioner);
    

答案 2 :(得分:0)

不完全是redux-thunk但在功能上相当于使用 redux-background。我创建的一个包,使这个过程更容易。

它是这样的:

import { startJob } from 'redux-background';
import store from './store';

store.dispatch(startJob('provision', function(job, dispatch, getState) {
  const { progress, data } = job;
  return new Promise((resolve, reject) => {
    // Do some async stuff
    // Report progress
    progress(10);

    // Return value
    return 10;
  })
}, { data: '...' } );

在你的状态结束时你应该有这样的东西

{
  background: {
   provision: {
     active: true,
     running: false,
     value: 10,
     error: null,
     /* and lot of more metadata */
     ....
   },
  }
}